From fc359d53e04c441989ac7348cf5378a7d0c32bc6 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Sat, 20 Mar 2021 18:47:12 -0400 Subject: [PATCH] 4.20.79 * Added: Setting to control the display of animated emotes. Before you all get excited, this is for better integration with the `BetterTTV Emotes` add-on as well as any future add-ons with animated emotes. * Added: Support for "Native Sort" for the emote menu, which uses the order from the API response. * Added: Quick Navigation for the emote menu, which places a list of emote sets along the right side. * Fixed: Skin tone picker for emoji in the emote menu not appearing correctly. * Fixed: Center the FFZ Control Center correctly when opening it. * Fixed: Modify the DOM we're emitting on clips pages for chat lines. Fixes night/betterttv#4416 * API Added: Support for animated images for emotes. --- package.json | 2 +- src/modules/chat/emotes.js | 89 ++++- src/modules/chat/index.js | 43 +- src/modules/chat/tokenizers.jsx | 77 +++- src/sites/clips/line.jsx | 54 +-- .../modules/chat/emote_menu.jsx | 373 +++++++++++++++--- .../twitch-twilight/modules/chat/line.js | 4 + .../modules/css_tweaks/styles/portrait.scss | 6 + src/sites/twitch-twilight/styles/chat.scss | 24 ++ .../styles/color_normalizer.scss | 1 + styles/dialog.scss | 10 +- styles/input/text.scss | 20 +- 12 files changed, 602 insertions(+), 101 deletions(-) diff --git a/package.json b/package.json index 78a3bd4e..3872b0c8 100755 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "frankerfacez", "author": "Dan Salvato LLC", - "version": "4.20.78", + "version": "4.20.79", "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 e4e32d18..9ad40159 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -12,6 +12,9 @@ import {NEW_API, API_SERVER, IS_OSX, EmoteTypes, TWITCH_GLOBAL_SETS, TWITCH_POIN import GET_EMOTE from './emote_info.gql'; import GET_EMOTE_SET from './emote_set_info.gql'; +const HoverRAF = Symbol('FFZ:Hover:RAF'); +const HoverState = Symbol('FFZ:Hover:State'); + const MOD_KEY = IS_OSX ? 'metaKey' : 'ctrlKey'; const MODIFIERS = { @@ -133,6 +136,8 @@ export default class Emotes extends Module { // Because this may be used elsewhere. this.handleClick = this.handleClick.bind(this); + this.animHover = this.animHover.bind(this); + this.animLeave = this.animLeave.bind(this); } onEnable() { @@ -249,6 +254,68 @@ export default class Emotes extends Module { } + // ======================================================================== + // Animation Hover + // ======================================================================== + + animHover(event) { // eslint-disable-line class-methods-use-this + const target = event.currentTarget; + if ( target[HoverState] ) + return; + + if ( target[HoverRAF] ) + cancelAnimationFrame(target[HoverRAF]); + + target[HoverRAF] = requestAnimationFrame(() => { + target[HoverRAF] = null; + if ( target[HoverState] ) + return; + + if ( ! target.matches(':hover') ) + return; + + target[HoverState] = true; + const emotes = target.querySelectorAll('.ffz-hover-emote'); + for(const em of emotes) { + const ds = em.dataset; + if ( ds.normalSrc && ds.hoverSrc ) { + em.src = ds.hoverSrc; + em.srcset = ds.hoverSrcSet; + } + } + }); + } + + + animLeave(event) { // eslint-disable-line class-methods-use-this + const target = event.currentTarget; + if ( ! target[HoverState] ) + return; + + if ( target[HoverRAF] ) + cancelAnimationFrame(target[HoverRAF]); + + target[HoverRAF] = requestAnimationFrame(() => { + target[HoverRAF] = null; + if ( ! target[HoverState] ) + return; + + if ( target.matches(':hover') ) + return; + + target[HoverState] = false; + const emotes = target.querySelectorAll('.ffz-hover-emote'); + for(const em of emotes) { + const ds = em.dataset; + if ( ds.normalSrc ) { + em.src = ds.normalSrc; + em.srcset = ds.normalSrcSet; + } + } + }); + } + + // ======================================================================== // Favorite Checking // ======================================================================== @@ -724,6 +791,7 @@ export default class Emotes extends Module { } emote.set_id = set_id; + emote.src = emote.urls[1]; emote.srcSet = `${emote.urls[1]} 1x`; if ( emote.urls[2] ) emote.srcSet += `, ${emote.urls[2]} 2x`; @@ -738,16 +806,35 @@ export default class Emotes extends Module { emote.srcSet2 += `, ${emote.urls[4]} 2x`; } + if ( emote.animated?.[1] ) { + emote.animSrc = emote.animated[1]; + emote.animSrcSet = `${emote.animated[1]} 1x`; + if ( emote.animated[2] ) { + emote.animSrcSet += `, ${emote.animated[2]} 2x`; + emote.animSrc2 = emote.animated[2]; + emote.animSrcSet2 = `${emote.animated[2]} 1x`; + + if ( emote.animated[4] ) { + emote.animSrcSet += `, ${emote.animated[4]} 4x`; + emote.animSrcSet2 += `, ${emote.animated[4]} 2x`; + } + } + } + emote.token = { type: 'emote', id: emote.id, set: set_id, provider: 'ffz', - src: emote.urls[1], + src: emote.src, srcSet: emote.srcSet, can_big: !! emote.urls[2], src2: emote.src2, srcSet2: emote.srcSet2, + animSrc: emote.animSrc, + animSrcSet: emote.animSrcSet, + animSrc2: emote.animSrc2, + animSrcSet2: emote.animSrcSet2, text: emote.hidden ? '???' : emote.name, length: emote.name.length, height: emote.height diff --git a/src/modules/chat/index.js b/src/modules/chat/index.js index fad5f6c0..69794123 100644 --- a/src/modules/chat/index.js +++ b/src/modules/chat/index.js @@ -918,8 +918,49 @@ export default class Chat extends Module { } }); + this.settings.add('chat.emotes.animated', { + default: null, + process(ctx, val) { + if ( val == null ) + val = ctx.get('ffzap.betterttv.gif_emoticons_mode') === 2 ? 1 : 0; + return val; + }, + ui: { + path: 'Chat > Appearance >> Emotes', + title: 'Animated Emotes', + description: 'This controls whether or not animated emotes are allowed to play in chat. When this is `Disabled`, emotes will appear as static images. Setting this to `Enable on Hover` may cause performance issues.', + component: 'setting-select-box', + data: [ + {value: 0, title: 'Disabled'}, + {value: 1, title: 'Enabled'}, + {value: 2, title: 'Enable on Hover'} + ] + } + }); + + this.settings.add('tooltip.emote-images.animated', { + requires: ['chat.emotes.animated'], + default: null, + process(ctx, val) { + if ( val == null ) + val = ctx.get('chat.emotes.animated') ? true : false; + return val; + }, + ui: { + path: 'Chat > Tooltips >> Emotes', + title: 'Display animated images of emotes.', + description: 'If this is not overridden, animated images are only shown in emote tool-tips if [Chat > Appearance >> Emotes > Animated Emotes](~chat.appearance.emotes) is not disabled.', + component: 'setting-check-box' + } + }); + this.settings.add('chat.bits.animated', { - default: true, + requires: ['chat.emotes.animated'], + default: null, + process(ctx, val) { + if ( val == null ) + val = ctx.get('chat.emotes.animated') ? true : false + }, ui: { path: 'Chat > Bits and Cheering >> Appearance', diff --git a/src/modules/chat/tokenizers.jsx b/src/modules/chat/tokenizers.jsx index 0f88a165..027968ef 100644 --- a/src/modules/chat/tokenizers.jsx +++ b/src/modules/chat/tokenizers.jsx @@ -1091,12 +1091,30 @@ export const CheerEmotes = { // ============================================================================ const render_emote = (token, createElement, wrapped) => { + const hover = token.anim === 2; + let src, srcSet, hoverSrc, hoverSrcSet, normalSrc, normalSrcSet; + + if ( token.anim === 1 && token.animSrc ) { + src = token.big ? token.animSrc2 : token.animSrc; + srcSet = token.big ? token.animSrcSet2 : token.animSrcSet; + } else { + src = token.big ? token.src2 : token.src; + srcSet = token.big ? token.srcSet2 : token.srcSet; + } + + if ( hover && token.animSrc ) { + normalSrc = src; + normalSrcSet = srcSet; + hoverSrc = token.big ? token.animSrc2 : token.animSrc; + hoverSrcSet = token.big ? token.animSrcSet2 : token.animSrcSet; + } + const mods = token.modifiers || [], ml = mods.length, emote = createElement('img', { - class: `${EMOTE_CLASS} ffz-tooltip${token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`, + class: `${EMOTE_CLASS} ffz-tooltip${hoverSrc ? ' ffz-hover-emote' : ''}${token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`, attrs: { - src: token.big && token.src2 || token.src, - srcSet: token.big && token.srcSet2 || token.srcSet, + src, + srcSet, alt: token.text, height: (token.big && ! token.can_big && token.height) ? `${token.height * 2}px` : undefined, 'data-tooltip-type': 'emote', @@ -1105,6 +1123,10 @@ const render_emote = (token, createElement, wrapped) => { 'data-set': token.set, 'data-code': token.code, 'data-variant': token.variant, + 'data-normal-src': normalSrc, + 'data-normal-src-set': normalSrcSet, + 'data-hover-src': hoverSrc, + 'data-hover-src-set': hoverSrcSet, 'data-modifiers': ml ? mods.map(x => x.id).join(' ') : null, 'data-modifier-info': ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null } @@ -1147,11 +1169,29 @@ export const AddonEmotes = { }, render(token, createElement, wrapped) { + const hover = token.anim === 2; + let src, srcSet, hoverSrc, hoverSrcSet, normalSrc, normalSrcSet; + + if ( token.anim === 1 && token.animSrc ) { + src = token.big ? token.animSrc2 : token.animSrc; + srcSet = token.big ? token.animSrcSet2 : token.animSrcSet; + } else { + src = token.big ? token.src2 : token.src; + srcSet = token.big ? token.srcSet2 : token.srcSet; + } + + if ( hover && token.animSrc ) { + normalSrc = src; + normalSrcSet = srcSet; + hoverSrc = token.big ? token.animSrc2 : token.animSrc; + hoverSrcSet = token.big ? token.animSrcSet2 : token.animSrcSet; + } + const mods = token.modifiers || [], ml = mods.length, emote = ({token.text} x.id).join(' ') : null} data-modifier-info={ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null} onClick={this.emotes.handleClick} @@ -1265,10 +1309,19 @@ export const AddonEmotes = { 'emote.owner', 'By: {owner}', {owner: emote.owner.display_name}); - if ( emote.urls[4] ) - preview = emote.urls[4]; - else if ( emote.urls[2] ) - preview = emote.urls[2]; + const anim = this.context.get('tooltip.emote-images.animated'); + if ( anim && emote.animated?.[1] ) { + if ( emote.animated[4] ) + preview = emote.animated[4]; + else if ( emote.animated[2] ) + preview = emote.animated[2]; + + } else { + if ( emote.urls[4] ) + preview = emote.urls[4]; + else if ( emote.urls[2] ) + preview = emote.urls[2]; + } } } else if ( provider === 'emoji' ) { @@ -1341,6 +1394,7 @@ export const AddonEmotes = { return tokens; const big = this.context.get('chat.emotes.2x'), + anim = this.context.get('chat.emotes.animated'), out = []; let last_token, emote; @@ -1391,7 +1445,8 @@ export const AddonEmotes = { const t = Object.assign({ modifiers: [], - big + big, + anim }, emote.token); out.push(t); last_token = t; diff --git a/src/sites/clips/line.jsx b/src/sites/clips/line.jsx index d9788f1d..c09a27ad 100644 --- a/src/sites/clips/line.jsx +++ b/src/sites/clips/line.jsx @@ -34,6 +34,7 @@ export default class Line extends Module { onEnable() { 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); @@ -64,6 +65,7 @@ export default class Line extends Module { const msg = t.standardizeMessage(this.props.node, this.props.video), + anim_hover = t.chat.context.get('chat.emotes.animated') === 2, is_action = msg.is_action, user = msg.user, color = t.parent.colors.process(user.color), @@ -72,31 +74,35 @@ export default class Line extends Module { const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u); - return (
- { - t.chat.badges.render(msg, createElement) - } - +
- { user.displayName } - {user.isIntl && } - -
{ - is_action ? '' : ':' - }
- { - t.chat.renderTokens(tokens, createElement) - } + { + t.chat.badges.render(msg, createElement) + } + + { user.displayName } + {user.isIntl && } + +
{ + is_action ? '' : ':' + }
+ { + t.chat.renderTokens(tokens, createElement) + } +
); } catch(err) { diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx index be933170..d32ffe24 100644 --- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx +++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx @@ -93,6 +93,38 @@ const EMOTE_SORTERS = [ if ( a.id > b.id ) return -1; if ( a.id < b.id ) return 1; return 0; + }, + function native_asc(a, b) { + if ( a.order || b.order ) { + if ( a.order && ! b.order ) return -1; + if ( b.order && ! a.order ) return 1; + + if ( a.order < b.order ) return -1; + if ( a.order > b.order ) return 1; + } + + if ( COLLATOR ) + return COLLATOR.compare(a.id, b.id); + + if ( a.id < b.id ) return -1; + if ( a.id > b.id ) return 1; + return 0; + }, + function native_desc(a, b) { + if ( a.order || b.order ) { + if ( a.order && ! b.order ) return 1; + if ( b.order && ! a.order ) return -1; + + if ( a.order < b.order ) return 1; + if ( a.order > b.order ) return -1; + } + + if ( COLLATOR ) + return COLLATOR.compare(a.id, b.id); + + if ( a.id < b.id ) return 1; + if ( a.id > b.id ) return -1; + return 0; } ]; @@ -167,6 +199,15 @@ export default class EmoteMenu extends Module { } }); + this.settings.add('chat.emote-menu.show-quick-nav', { + default: false, + ui: { + path: 'Chat > Emote Menu >> Appearance', + title: 'Show a quick navigation bar along the side of the menu.', + component: 'setting-check-box' + } + }); + this.settings.add('chat.emote-menu.show-heading', { default: 1, ui: { @@ -227,12 +268,14 @@ export default class EmoteMenu extends Module { this.settings.add('chat.emote-menu.sort-emotes', { - default: 0, + default: 4, ui: { path: 'Chat > Emote Menu >> Sorting', title: 'Sort Emotes By', component: 'setting-select-box', data: [ + {value: 4, title: 'Native Order, Ascending'}, + {value: 5, title: 'Native Order, Descending'}, {value: 0, title: 'Order Added (ID), Ascending'}, {value: 1, title: 'Order Added (ID), Descending'}, {value: 2, title: 'Name, Ascending'}, @@ -275,7 +318,7 @@ export default class EmoteMenu extends Module { this.chat.context.on('changed:chat.emote-menu.enabled', () => this.EmoteMenu.forceUpdate()); - const fup = () => this.MenuWrapper.forceUpdate(); + //const fup = () => this.MenuWrapper.forceUpdate(); const rebuild = () => { for(const inst of this.MenuWrapper.instances) inst.rebuildData(); @@ -284,10 +327,10 @@ export default class EmoteMenu extends Module { this.chat.context.on('changed:chat.fix-bad-emotes', rebuild); this.chat.context.on('changed:chat.emote-menu.sort-emotes', rebuild); this.chat.context.on('changed:chat.emote-menu.sort-tiers-last', rebuild); - this.chat.context.on('changed:chat.emote-menu.show-heading', fup); - this.chat.context.on('changed:chat.emote-menu.show-search', fup); - this.chat.context.on('changed:chat.emote-menu.reduced-padding', fup); - this.chat.context.on('changed:chat.emote-menu.combine-tabs', fup); + //this.chat.context.on('changed:chat.emote-menu.show-heading', fup); + //this.chat.context.on('changed:chat.emote-menu.show-search', fup); + //this.chat.context.on('changed:chat.emote-menu.reduced-padding', fup); + //this.chat.context.on('changed:chat.emote-menu.combine-tabs', fup); this.chat.context.on('changed:chat.emoji.style', this.updateEmojiVariables, this); @@ -380,6 +423,40 @@ export default class EmoteMenu extends Module { React = this.web_munch.getModule('react'), createElement = React && React.createElement; + this.EmoteModifierPicker = class FFZEmoteModifierPicker extends React.Component { + constructor(props) { + super(props); + + this.onClickOutside = () => this.props.close(); + + this.element = null; + this.saveRef = element => this.element = element; + + this.state = { + + }; + } + + componentDidMount() { + if ( this.element ) + this._clicker = new ClickOutside(this.element, this.onClickOutside); + } + + componentWillUnmount() { + if ( this._clicker ) { + this._clicker.destroy(); + this._clicker = null; + } + } + + render() { + return (
+
+
+
) + } + } + this.EmojiTonePicker = class FFZEmojiTonePicker extends React.Component { constructor(props) { super(props); @@ -446,7 +523,7 @@ export default class EmoteMenu extends Module { const tones = Object.entries(emoji.variants).map(([tone, emoji]) => this.renderTone(emoji, tone)); - return (
+ return (
{this.renderTone(emoji, null)} {tones} @@ -514,6 +591,7 @@ export default class EmoteMenu extends Module { this.state = { active: false, + open_menu: null, activeEmote: -1, hidden: hidden && props.data && hidden.includes(props.data.hide_key || props.data.key), collapsed: collapsed && props.data && collapsed.includes(props.data.key), @@ -523,6 +601,8 @@ export default class EmoteMenu extends Module { this.keyHeading = this.keyHeading.bind(this); this.clickHeading = this.clickHeading.bind(this); this.clickEmote = this.clickEmote.bind(this); + this.contextEmote = this.contextEmote.bind(this); + this.closeEmoteModMenu = this.closeEmoteModMenu.bind(this); this.mouseEnter = () => this.state.intersecting || this.setState({intersecting: true}); @@ -578,6 +658,29 @@ export default class EmoteMenu extends Module { this.props.onClickToken(event.currentTarget.dataset.name) } + contextEmote(event) { + if ( event.ctrlKey || event.shiftKey ) + return; + + event.preventDefault(); + + const ds = event.currentTarget.dataset; + if ( ds.provider !== 'twitch' ) + return; + + const modifiers = this.props.emote_modifiers[ds.id]; + if ( Array.isArray(modifiers) && modifiers.length ) + this.setState({ + open_menu: ds.id + }); + } + + closeEmoteModMenu() { + this.setState({ + open_menu: null + }); + } + keyHeading(event) { if ( event.keyCode === KEYS.Enter || event.keyCode === KEYS.Space ) this.clickHeading(); @@ -639,7 +742,7 @@ export default class EmoteMenu extends Module { filtered = this.props.filtered, visibility = this.props.visibility_control; - let show_heading = ! (data.is_favorites && ! t.chat.context.get('chat.emote-menu.combine-tabs')) && t.chat.context.get('chat.emote-menu.show-heading'); + let show_heading = ! (data.is_favorites && ! this.props.combineTabs) && this.props.showHeading; if ( show_heading === 2 ) show_heading = ! filtered; else @@ -758,8 +861,21 @@ export default class EmoteMenu extends Module { return ; const visibility = this.props.visibility_control, + modifiers = this.props.emote_modifiers[emote.id], + has_modifiers = Array.isArray(modifiers) && modifiers.length > 0, + has_menu = has_modifiers && this.state.open_menu == emote.id, + animated = this.props.animated, hidden = visibility && emote.hidden; + let src, srcSet; + if ( animated && emote.animSrc ) { + src = emote.animSrc; + srcSet = emote.animSrcSet; + } else { + src = emote.src; + srcSet = emote.srcSet; + } + return () } @@ -944,6 +1067,11 @@ export default class EmoteMenu extends Module { constructor(props) { super(props); + this.nav_ref = null; + this.saveNavRef = ref => { + this.nav_ref = ref; + } + this.ref = null; this.saveScrollRef = ref => { this.ref = ref; @@ -955,6 +1083,13 @@ export default class EmoteMenu extends Module { this.state = { tab: null, + active_nav: null, + quickNav: t.chat.context.get('chat.emote-menu.show-quick-nav'), + animated: t.chat.context.get('chat.emotes.animated'), + showHeading: t.chat.context.get('chat.emote-menu.show-heading'), + reducedPadding: t.chat.context.get('chat.emote-menu.reduced-padding'), + combineTabs: t.chat.context.get('chat.emote-menu.combine-tabs'), + showSearch: t.chat.context.get('chat.emote-menu.show-search'), tone: t.settings.provider.get('emoji-tone', null) } @@ -986,6 +1121,7 @@ export default class EmoteMenu extends Module { this.handleObserve = this.handleObserve.bind(this); this.pickTone = this.pickTone.bind(this); this.clickTab = this.clickTab.bind(this); + this.clickSideNav = this.clickSideNav.bind(this); //this.clickRefresh = this.clickRefresh.bind(this); this.handleFilterChange = this.handleFilterChange.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); @@ -1032,22 +1168,39 @@ export default class EmoteMenu extends Module { this.observer = this._observed = null; } + scrollNavIntoView() { + requestAnimationFrame(() => { + const el = this.nav_ref?.querySelector?.(`button[data-key="${this.state.active_nav}"]`); + if ( el ) + el.scrollIntoView({block: 'nearest'}); + }); + } + handleObserve(event) { - let changed = false; + let changed = false, + active = this.state.active_nav; for(const entry of event) { - const inst = this.observing.get(entry.target); - if ( ! inst || inst.state.intersecting === entry.isIntersecting ) + const inst = this.observing.get(entry.target), + intersecting = entry.isIntersecting; + if ( ! inst || inst.state.intersecting === intersecting ) continue; changed = true; - inst.setState({ - intersecting: entry.isIntersecting - }); + inst.setState({intersecting}); + + if ( intersecting ) + active = inst.props?.data?.key; } - if ( changed ) + if ( changed ) { requestAnimationFrame(clearTooltips); + + if ( ! this.lock_active && active !== this.state.active_nav ) + this.setState({ + active_nav: active + }, () => this.scrollNavIntoView()); + } } startObserving(element, inst) { @@ -1078,16 +1231,41 @@ export default class EmoteMenu extends Module { if ( this.ref ) this.createObserver(); + t.chat.context.on('changed:chat.emotes.animated', this.updateSettingState, this); + t.chat.context.on('changed:chat.emote-menu.show-quick-nav', this.updateSettingState, this); + t.chat.context.on('changed:chat.emote-menu.reduced-padding', this.updateSettingState, this); + t.chat.context.on('changed:chat.emote-menu.show-heading', this.updateSettingState, this); + t.chat.context.on('changed:chat.emote-menu.combine-tabs', this.updateSettingState, this); + t.chat.context.on('changed:chat.emote-menu.show-search', this.updateSettingState, this); + window.ffz_menu = this; } componentWillUnmount() { this.destroyObserver(); + t.chat.context.off('changed:chat.emotes.animated', this.updateSettingState, this); + t.chat.context.off('changed:chat.emote-menu.show-quick-nav', this.updateSettingState, this); + t.chat.context.off('changed:chat.emote-menu.show-heading', this.updateSettingState, this); + t.chat.context.off('changed:chat.emote-menu.reduced-padding', this.updateSettingState, this); + t.chat.context.off('changed:chat.emote-menu.combine-tabs', this.updateSettingState, this); + t.chat.context.off('changed:chat.emote-menu.show-search', this.updateSettingState, this); + if ( window.ffz_menu === this ) window.ffz_menu = null; } + updateSettingState() { + this.setState({ + quickNav: t.chat.context.get('chat.emote-menu.show-quick-nav'), + animated: t.chat.context.get('chat.emotes.animated'), + showHeading: t.chat.context.get('chat.emote-menu.show-heading'), + reducedPadding: t.chat.context.get('chat.emote-menu.reduced-padding'), + combineTabs: t.chat.context.get('chat.emote-menu.combine-tabs'), + showSearch: t.chat.context.get('chat.emote-menu.show-search') + }); + } + pickTone(tone) { tone = tone || null; t.settings.provider.set('emoji-tone', tone); @@ -1100,9 +1278,22 @@ export default class EmoteMenu extends Module { )); } + clickSideNav(event) { + const key = event.currentTarget.dataset.key; + const el = this.ref?.querySelector?.(`section[data-key="${key}"]`); + if ( el ) { + this.lock_active = true; + el.scrollIntoView(); + this.setState({ + active_nav: key + }); + setTimeout(() => this.lock_active = false, 250); + } + } + clickTab(event) { const tab = event.currentTarget.dataset.tab; - if ( t.chat.context.get('chat.emote-menu.combine-tabs') ) { + if ( this.state.combineTabs ) { let sets; switch(tab) { case 'fav': @@ -1427,6 +1618,7 @@ export default class EmoteMenu extends Module { const state = Object.assign({}, old_state), data = state.set_data || {}, + modifiers = state.emote_modifiers = {}, channel = state.channel_sets = [], all = state.all_sets = [], favorites = state.favorites = []; @@ -1634,6 +1826,8 @@ export default class EmoteMenu extends Module { section.bad = true; } + let order = 0; + for(const emote of emote_set.emotes) { // Validate emotes, because apparently Twitch is handing // out bad emote data. @@ -1657,6 +1851,9 @@ export default class EmoteMenu extends Module { srcSet = `${src} 1x, ${base}/2.0 2x` } + /*if ( Array.isArray(emote.modifiers) && emote.modifiers.length ) + modifiers[id] = emote.modifiers;*/ + const em = { provider: 'twitch', id, @@ -1664,6 +1861,7 @@ export default class EmoteMenu extends Module { name, src, srcSet, + order: order++, overridden: overridden ? mapped.id : null, misc: ! chan, bits: is_bits, @@ -1751,6 +1949,8 @@ export default class EmoteMenu extends Module { else section.all_locked = false; + let order = 0; + for(const emote of product.emotes) { // Validate emotes, because apparently Twitch is handing // out bad emote data. @@ -1763,11 +1963,15 @@ export default class EmoteMenu extends Module { seen = twitch_seen.has(id), is_fav = twitch_favorites.includes(id); + /*if ( Array.isArray(emote.modifiers) && emote.modifiers.length ) + modifiers[id] = emote.modifiers;*/ + const em = { provider: 'twitch', id, set_id, name, + order: order++, locked: locked && ! seen, src: `${base}/1.0`, srcSet: `${base}/1.0 1x, ${base}/2.0 2x`, @@ -1791,6 +1995,7 @@ export default class EmoteMenu extends Module { const seen_bits = new Set; if ( Array.isArray(bits) ) { + let order; for(const emote of bits) { if ( ! emote || ! emote.id || ! emote.bitsBadgeTierSummary ) continue; @@ -1816,12 +2021,16 @@ export default class EmoteMenu extends Module { const base = `${TWITCH_EMOTE_BASE}${id}`, is_fav = twitch_favorites.includes(id); + /*if ( Array.isArray(emote.modifiers) && emote.modifiers.length ) + modifiers[id] = emote.modifiers;*/ + const em = { provider: 'twitch', id, set_id, name: emote.token, locked, + order: order++, src: `${base}/1.0`, srcSet: `${base}/1.0 1x, ${base}/2.0 2x`, bits: true, @@ -1963,8 +2172,10 @@ export default class EmoteMenu extends Module { provider: 'ffz', id: emote.id, set_id: emote_set.id, - src: emote.urls[1], + src: emote.src, srcSet: emote.srcSet, + animSrc: emote.animSrc, + animSrcSet: emote.animSrcSet, name: emote.name, favorite: is_fav, hidden: known_hidden.includes(emote.id), @@ -2050,10 +2261,10 @@ export default class EmoteMenu extends Module { return null; const loading = this.state.loading || this.props.loading, - padding = t.chat.context.get('chat.emote-menu.reduced-padding'), - no_tabs = t.chat.context.get('chat.emote-menu.combine-tabs'); + padding = this.state.reducedPadding, //t.chat.context.get('chat.emote-menu.reduced-padding'), + no_tabs = this.state.combineTabs; //t.chat.context.get('chat.emote-menu.combine-tabs'); - let tab, sets, is_emoji; + let tab, sets, is_emoji, is_favs; if ( no_tabs ) { sets = [ @@ -2069,6 +2280,7 @@ export default class EmoteMenu extends Module { tab = 'all'; is_emoji = tab === 'emoji'; + is_favs = tab === 'fav'; switch(tab) { case 'fav': @@ -2097,34 +2309,89 @@ export default class EmoteMenu extends Module { role="dialog" >
-
-
-
- {loading && this.renderLoading()} - {!loading && sets && sets.map(data => data && (! visibility || (! data.emoji && ! data.is_favorites)) && createElement( - data.emoji ? t.EmojiSection : t.MenuSection, - { - key: data.key, - data, - filtered: this.state.filtered, - visibility_control: visibility, - onClickToken: this.props.onClickToken, - addSection: this.addSection, - removeSection: this.removeSection, - startObserving: this.startObserving, - stopObserving: this.stopObserving - } - ))} - {! loading && (! sets || ! sets.length) && this.renderEmpty()} +
+
+
+
+ {loading && this.renderLoading()} + {!loading && sets && sets.map((data,idx) => data && (! visibility || (! data.emoji && ! data.is_favorites)) && createElement( + data.emoji ? t.EmojiSection : t.MenuSection, + { + key: data.key, + idx, + data, + emote_modifiers: this.state.emote_modifiers, + animated: this.state.animated, + combineTabs: this.state.combineTabs, + showHeading: this.state.showHeading, + filtered: this.state.filtered, + visibility_control: visibility, + onClickToken: this.props.onClickToken, + addSection: this.addSection, + removeSection: this.removeSection, + startObserving: this.startObserving, + stopObserving: this.stopObserving + } + ))} + {! loading && (! sets || ! sets.length) && this.renderEmpty()} +
+ {(! loading && this.state.quickNav && ! is_favs) && (
+
+
+
+ {!loading && sets && sets.map(data => { + if ( ! data || (visibility && (data.is_favorites || data.emoji)) ) + return null; + + const active = this.state.active_nav === data.key; + + return (); + })} + {no_tabs &&
} + {no_tabs && ()} +
+
+
+
)}
- {(is_emoji || t.chat.context.get('chat.emote-menu.show-search')) && (
+ {(is_emoji || this.state.showSearch) && (
+ {(no_tabs || is_emoji) && ! visibility && this.state.has_emoji_tab && } {(no_tabs || ! is_emoji) &&
} - {(no_tabs || is_emoji) && ! visibility && this.state.has_emoji_tab && }
)} -
+ {(no_tabs && this.state.quickNav) ? null : (
{! visibility &&
-
+
)}
diff --git a/src/sites/twitch-twilight/modules/chat/line.js b/src/sites/twitch-twilight/modules/chat/line.js index b6172077..8c5a8074 100644 --- a/src/sites/twitch-twilight/modules/chat/line.js +++ b/src/sites/twitch-twilight/modules/chat/line.js @@ -61,6 +61,7 @@ export default class ChatLine extends Module { this.on('i18n:update', 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); @@ -337,6 +338,7 @@ export default class ChatLine extends Module { const types = t.parent.message_types || {}, deleted_count = this.props.deletedCount, reply_mode = t.chat.context.get('chat.replies.style'), + anim_hover = t.chat.context.get('chat.emotes.animated') === 2, override_mode = t.chat.context.get('chat.filtering.display-deleted'), msg = t.chat.standardizeMessage(this.props.message), @@ -892,6 +894,8 @@ other {# messages were deleted by a moderator.} 'data-room': room, 'data-user-id': user.userID, 'data-user': user.userLogin && user.userLogin.toLowerCase(), + onMouseOver: anim_hover ? t.chat.emotes.animHover : null, + onMouseOut: anim_hover ? t.chat.emotes.animLeave : null }, out); } catch(err) { diff --git a/src/sites/twitch-twilight/modules/css_tweaks/styles/portrait.scss b/src/sites/twitch-twilight/modules/css_tweaks/styles/portrait.scss index 5ac3e4c8..68c86e9c 100644 --- a/src/sites/twitch-twilight/modules/css_tweaks/styles/portrait.scss +++ b/src/sites/twitch-twilight/modules/css_tweaks/styles/portrait.scss @@ -117,10 +117,15 @@ transform: rotate(90deg); } + .emote-picker__nav-content-overflow, .emote-picker__tab-content { max-height: calc(var(--ffz-chat-height) - 26rem); } + .emote-picker__nav-content-overflow { + height: 100% !important; + } + &.right-column--theatre { .ffz--portrait-invert & { bottom: unset !important; @@ -132,6 +137,7 @@ height: calc(100vh - var(--ffz-theater-height)) !important; + .emote-picker__nav-content-overflow, .emote-picker__tab-content { max-height: calc(calc(100vh - var(--ffz-theater-height)) - 26rem); } diff --git a/src/sites/twitch-twilight/styles/chat.scss b/src/sites/twitch-twilight/styles/chat.scss index bf4bb318..fbb8ecb1 100644 --- a/src/sites/twitch-twilight/styles/chat.scss +++ b/src/sites/twitch-twilight/styles/chat.scss @@ -269,6 +269,25 @@ } } +.ffz-emote-picker--nav-icon { + font-size: 2rem; + line-height: 1em; + + position: relative; + height: 2rem; + + &:before { + position: absolute; + left: 50%; + top: 50%; + transform: translate(-50%, -50%); + + width: unset !important; + font-size: unset !important; + margin: 0 !important; + } +} + .ffz--emote-picker { section:not(.filtered) heading { cursor: pointer; @@ -311,6 +330,11 @@ } @media only screen and (max-height: 750px) { + .emote-picker__nav-content-overflow { + height: 100% !important; + } + + .emote-picker__nav-content-overflow, .emote-picker__tab-content { #root & { max-height: calc(100vh - 31rem); diff --git a/src/sites/twitch-twilight/styles/color_normalizer.scss b/src/sites/twitch-twilight/styles/color_normalizer.scss index cc36d1d4..faeb02ca 100644 --- a/src/sites/twitch-twilight/styles/color_normalizer.scss +++ b/src/sites/twitch-twilight/styles/color_normalizer.scss @@ -12,6 +12,7 @@ .clmgr-table__row, .sunlight-expanded-nav-drop-down-menu-layout__scrollable-area, .stream-manager--page-view .mosaic-window-body, + .emote-grid-section__header-title, .ach-card, .ach-card--expanded .ach-card__inner, .room-upsell, diff --git a/styles/dialog.scss b/styles/dialog.scss index 233e0c0e..1ac5d4f4 100644 --- a/styles/dialog.scss +++ b/styles/dialog.scss @@ -6,18 +6,20 @@ z-index: 99999999; - height: 50vh; - width: 50vw; + --width: #{"min(75vw, 128rem)"}; + --height: #{"min(75vh, calc(0.75 * var(--width)))"}; + + top: #{"calc(calc(100vh - var(--height)) / 2)"}; + left: #{"calc(calc(100vw - var(--width)) / 2)"}; min-width: 64rem; min-height: 30rem; - --width: #{"min(75vw, 128rem)"}; width: 75vw; width: var(--width); height: 50vh; - height: #{"min(75vh, calc(0.75 * var(--width)))"}; + height: var(--height); > header { cursor: move; diff --git a/styles/input/text.scss b/styles/input/text.scss index 464b323f..4d08e469 100644 --- a/styles/input/text.scss +++ b/styles/input/text.scss @@ -221,12 +221,20 @@ max-width: 100%; width: auto; } -} -.ffz-textarea--error:focus { - box-shadow: var(--shadow-input-error-focus); -} + &--error { + box-shadow: var(--shadow-input-error); -.ffz-textarea--no-resize { - resize: none; + &,&:focus { + border: var(--border-width-input) solid var(--color-border-input-error); + } + + &:focus { + box-shadow: var(--shadow-input-error-focus); + } + } + + &--no-resize { + resize: none; + } } \ No newline at end of file