1
0
Fork 0
mirror of https://github.com/FrankerFaceZ/FrankerFaceZ.git synced 2025-06-28 05:15:54 +00:00
* 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.
This commit is contained in:
SirStendec 2021-03-20 18:47:12 -04:00
parent 5a5a68adb6
commit fc359d53e0
12 changed files with 602 additions and 101 deletions

View file

@ -1,7 +1,7 @@
{ {
"name": "frankerfacez", "name": "frankerfacez",
"author": "Dan Salvato LLC", "author": "Dan Salvato LLC",
"version": "4.20.78", "version": "4.20.79",
"description": "FrankerFaceZ is a Twitch enhancement suite.", "description": "FrankerFaceZ is a Twitch enhancement suite.",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",

View file

@ -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 from './emote_info.gql';
import GET_EMOTE_SET from './emote_set_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 MOD_KEY = IS_OSX ? 'metaKey' : 'ctrlKey';
const MODIFIERS = { const MODIFIERS = {
@ -133,6 +136,8 @@ export default class Emotes extends Module {
// Because this may be used elsewhere. // Because this may be used elsewhere.
this.handleClick = this.handleClick.bind(this); this.handleClick = this.handleClick.bind(this);
this.animHover = this.animHover.bind(this);
this.animLeave = this.animLeave.bind(this);
} }
onEnable() { 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 // Favorite Checking
// ======================================================================== // ========================================================================
@ -724,6 +791,7 @@ export default class Emotes extends Module {
} }
emote.set_id = set_id; emote.set_id = set_id;
emote.src = emote.urls[1];
emote.srcSet = `${emote.urls[1]} 1x`; emote.srcSet = `${emote.urls[1]} 1x`;
if ( emote.urls[2] ) if ( emote.urls[2] )
emote.srcSet += `, ${emote.urls[2]} 2x`; emote.srcSet += `, ${emote.urls[2]} 2x`;
@ -738,16 +806,35 @@ export default class Emotes extends Module {
emote.srcSet2 += `, ${emote.urls[4]} 2x`; 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 = { emote.token = {
type: 'emote', type: 'emote',
id: emote.id, id: emote.id,
set: set_id, set: set_id,
provider: 'ffz', provider: 'ffz',
src: emote.urls[1], src: emote.src,
srcSet: emote.srcSet, srcSet: emote.srcSet,
can_big: !! emote.urls[2], can_big: !! emote.urls[2],
src2: emote.src2, src2: emote.src2,
srcSet2: emote.srcSet2, srcSet2: emote.srcSet2,
animSrc: emote.animSrc,
animSrcSet: emote.animSrcSet,
animSrc2: emote.animSrc2,
animSrcSet2: emote.animSrcSet2,
text: emote.hidden ? '???' : emote.name, text: emote.hidden ? '???' : emote.name,
length: emote.name.length, length: emote.name.length,
height: emote.height height: emote.height

View file

@ -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', { 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: { ui: {
path: 'Chat > Bits and Cheering >> Appearance', path: 'Chat > Bits and Cheering >> Appearance',

View file

@ -1091,12 +1091,30 @@ export const CheerEmotes = {
// ============================================================================ // ============================================================================
const render_emote = (token, createElement, wrapped) => { 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, const mods = token.modifiers || [], ml = mods.length,
emote = createElement('img', { 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: { attrs: {
src: token.big && token.src2 || token.src, src,
srcSet: token.big && token.srcSet2 || token.srcSet, srcSet,
alt: token.text, alt: token.text,
height: (token.big && ! token.can_big && token.height) ? `${token.height * 2}px` : undefined, height: (token.big && ! token.can_big && token.height) ? `${token.height * 2}px` : undefined,
'data-tooltip-type': 'emote', 'data-tooltip-type': 'emote',
@ -1105,6 +1123,10 @@ const render_emote = (token, createElement, wrapped) => {
'data-set': token.set, 'data-set': token.set,
'data-code': token.code, 'data-code': token.code,
'data-variant': token.variant, '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-modifiers': ml ? mods.map(x => x.id).join(' ') : null,
'data-modifier-info': ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : 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) { 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, const mods = token.modifiers || [], ml = mods.length,
emote = (<img emote = (<img
class={`${EMOTE_CLASS} ffz--pointer-events ffz-tooltip${token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`} class={`${EMOTE_CLASS} ffz--pointer-events ffz-tooltip${hoverSrc ? ' ffz-hover-emote' : ''}${token.provider === 'ffz' ? ' ffz-emote' : token.provider === 'emoji' ? ' ffz-emoji' : ''}`}
src={token.big && token.src2 || token.src} src={src}
srcSet={token.big && token.srcSet2 || token.srcSet} srcSet={srcSet}
height={(token.big && ! token.can_big && token.height) ? `${token.height * 2}px` : undefined} height={(token.big && ! token.can_big && token.height) ? `${token.height * 2}px` : undefined}
alt={token.text} alt={token.text}
data-tooltip-type="emote" data-tooltip-type="emote"
@ -1160,6 +1200,10 @@ export const AddonEmotes = {
data-set={token.set} data-set={token.set}
data-code={token.code} data-code={token.code}
data-variant={token.variant} 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-modifiers={ml ? mods.map(x => x.id).join(' ') : null}
data-modifier-info={ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null} data-modifier-info={ml ? JSON.stringify(mods.map(x => [x.set, x.id])) : null}
onClick={this.emotes.handleClick} onClick={this.emotes.handleClick}
@ -1265,11 +1309,20 @@ export const AddonEmotes = {
'emote.owner', 'By: {owner}', 'emote.owner', 'By: {owner}',
{owner: emote.owner.display_name}); {owner: emote.owner.display_name});
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] ) if ( emote.urls[4] )
preview = emote.urls[4]; preview = emote.urls[4];
else if ( emote.urls[2] ) else if ( emote.urls[2] )
preview = emote.urls[2]; preview = emote.urls[2];
} }
}
} else if ( provider === 'emoji' ) { } else if ( provider === 'emoji' ) {
const emoji = this.emoji.emoji[ds.code], const emoji = this.emoji.emoji[ds.code],
@ -1341,6 +1394,7 @@ export const AddonEmotes = {
return tokens; return tokens;
const big = this.context.get('chat.emotes.2x'), const big = this.context.get('chat.emotes.2x'),
anim = this.context.get('chat.emotes.animated'),
out = []; out = [];
let last_token, emote; let last_token, emote;
@ -1391,7 +1445,8 @@ export const AddonEmotes = {
const t = Object.assign({ const t = Object.assign({
modifiers: [], modifiers: [],
big big,
anim
}, emote.token); }, emote.token);
out.push(t); out.push(t);
last_token = t; last_token = t;

View file

@ -34,6 +34,7 @@ export default class Line extends Module {
onEnable() { onEnable() {
this.chat.context.on('changed:chat.emotes.2x', 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.emoji.style', this.updateLines, this);
this.chat.context.on('changed:chat.bits.stack', 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.style', this.updateLines, this);
@ -64,6 +65,7 @@ export default class Line extends Module {
const msg = t.standardizeMessage(this.props.node, this.props.video), 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, is_action = msg.is_action,
user = msg.user, user = msg.user,
color = t.parent.colors.process(user.color), color = t.parent.colors.process(user.color),
@ -72,23 +74,26 @@ export default class Line extends Module {
const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u); const tokens = msg.ffz_tokens = msg.ffz_tokens || t.chat.tokenizeMessage(msg, u);
return (<div return (<div class="tw-mg-b-1" style={{marginBottom:'0 !important'}}>
<div
data-a-target="tw-animation-target" data-a-target="tw-animation-target"
class="ffz--clip-chat-line tw-animation tw-animation--animate tw-animation--duration-short tw-animation--fill-mode-both tw-animation--slide-in-bottom tw-animation--timing-ease" class="ffz--clip-chat-line tw-animation tw-animation--animate tw-animation--duration-short tw-animation--fill-mode-both tw-animation--slide-in-bottom tw-animation--timing-ease"
data-room-id={msg.roomID} data-room-id={msg.roomID}
data-room={msg.roomLogin} data-room={msg.roomLogin}
data-user-id={user.id} data-user-id={user.id}
data-user={user.login} data-user={user.login}
onMouseOver={anim_hover ? t.chat.emotes.animHover : null}
onMouseOut={anim_hover ? t.chat.emotes.animLeave : null}
> >
<span class="chat-line__message--badges">{ <span class="chat-line__message--badges">{
t.chat.badges.render(msg, createElement) t.chat.badges.render(msg, createElement)
}</span> }</span>
<a <a
class="clip-chat__message-author tw-font-size-5 tw-strong tw-link notranslate" class="clip-chat__message-author tw-font-size-5 tw-link notranslate"
href={`https://www.twitch.tv/${user.login}/clips`} href={`https://www.twitch.tv/${user.login}/clips`}
style={{color}} style={{color}}
> >
<span class="chat-author__display_name">{ user.displayName }</span> <span class="tw-strong chat-author__display_name">{ user.displayName }</span>
{user.isIntl && <span class="chat-author__intl-login"> ({user.login}) </span>} {user.isIntl && <span class="chat-author__intl-login"> ({user.login}) </span>}
</a> </a>
<div class="tw-inline-block tw-mg-r-05">{ <div class="tw-inline-block tw-mg-r-05">{
@ -97,6 +102,7 @@ export default class Line extends Module {
<span class="message" style={{color: is_action ? color : null}}>{ <span class="message" style={{color: is_action ? color : null}}>{
t.chat.renderTokens(tokens, createElement) t.chat.renderTokens(tokens, createElement)
}</span> }</span>
</div>
</div>); </div>);
} catch(err) { } catch(err) {

View file

@ -93,6 +93,38 @@ const EMOTE_SORTERS = [
if ( a.id > b.id ) return -1; if ( a.id > b.id ) return -1;
if ( a.id < b.id ) return 1; if ( a.id < b.id ) return 1;
return 0; 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', { this.settings.add('chat.emote-menu.show-heading', {
default: 1, default: 1,
ui: { ui: {
@ -227,12 +268,14 @@ export default class EmoteMenu extends Module {
this.settings.add('chat.emote-menu.sort-emotes', { this.settings.add('chat.emote-menu.sort-emotes', {
default: 0, default: 4,
ui: { ui: {
path: 'Chat > Emote Menu >> Sorting', path: 'Chat > Emote Menu >> Sorting',
title: 'Sort Emotes By', title: 'Sort Emotes By',
component: 'setting-select-box', component: 'setting-select-box',
data: [ data: [
{value: 4, title: 'Native Order, Ascending'},
{value: 5, title: 'Native Order, Descending'},
{value: 0, title: 'Order Added (ID), Ascending'}, {value: 0, title: 'Order Added (ID), Ascending'},
{value: 1, title: 'Order Added (ID), Descending'}, {value: 1, title: 'Order Added (ID), Descending'},
{value: 2, title: 'Name, Ascending'}, {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.chat.context.on('changed:chat.emote-menu.enabled', () =>
this.EmoteMenu.forceUpdate()); this.EmoteMenu.forceUpdate());
const fup = () => this.MenuWrapper.forceUpdate(); //const fup = () => this.MenuWrapper.forceUpdate();
const rebuild = () => { const rebuild = () => {
for(const inst of this.MenuWrapper.instances) for(const inst of this.MenuWrapper.instances)
inst.rebuildData(); 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.fix-bad-emotes', rebuild);
this.chat.context.on('changed:chat.emote-menu.sort-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.sort-tiers-last', rebuild);
this.chat.context.on('changed:chat.emote-menu.show-heading', 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.show-search', fup);
this.chat.context.on('changed:chat.emote-menu.reduced-padding', 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.combine-tabs', fup);
this.chat.context.on('changed:chat.emoji.style', this.updateEmojiVariables, this); 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'), React = this.web_munch.getModule('react'),
createElement = React && React.createElement; 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 (<div ref={this.saveRef} class="ffz--modifier-picker tw-absolute ffz-balloon tw-tooltip-down tw-tooltip--align-center ffz-balloon tw-block">
<div class="tw-border-b tw-border-l tw-border-r tw-border-t tw-border-radius-medium tw-c-background-base tw-elevation-1">
</div>
</div>)
}
}
this.EmojiTonePicker = class FFZEmojiTonePicker extends React.Component { this.EmojiTonePicker = class FFZEmojiTonePicker extends React.Component {
constructor(props) { constructor(props) {
super(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)); const tones = Object.entries(emoji.variants).map(([tone, emoji]) => this.renderTone(emoji, tone));
return (<div class="tw-absolute ffz-balloon ffz-balloon--up ffz-balloon--right ffz-balloon tw-block"> return (<div class="tw-absolute ffz-balloon tw-tooltip--up tw-tooltip--align-right ffz-balloon tw-block">
<div class="tw-border-b tw-border-l tw-border-r tw-border-t tw-border-radius-medium tw-c-background-base tw-elevation-1"> <div class="tw-border-b tw-border-l tw-border-r tw-border-t tw-border-radius-medium tw-c-background-base tw-elevation-1">
{this.renderTone(emoji, null)} {this.renderTone(emoji, null)}
{tones} {tones}
@ -514,6 +591,7 @@ export default class EmoteMenu extends Module {
this.state = { this.state = {
active: false, active: false,
open_menu: null,
activeEmote: -1, activeEmote: -1,
hidden: hidden && props.data && hidden.includes(props.data.hide_key || props.data.key), hidden: hidden && props.data && hidden.includes(props.data.hide_key || props.data.key),
collapsed: collapsed && props.data && collapsed.includes(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.keyHeading = this.keyHeading.bind(this);
this.clickHeading = this.clickHeading.bind(this); this.clickHeading = this.clickHeading.bind(this);
this.clickEmote = this.clickEmote.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}); 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) 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) { keyHeading(event) {
if ( event.keyCode === KEYS.Enter || event.keyCode === KEYS.Space ) if ( event.keyCode === KEYS.Enter || event.keyCode === KEYS.Space )
this.clickHeading(); this.clickHeading();
@ -639,7 +742,7 @@ export default class EmoteMenu extends Module {
filtered = this.props.filtered, filtered = this.props.filtered,
visibility = this.props.visibility_control; 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 ) if ( show_heading === 2 )
show_heading = ! filtered; show_heading = ! filtered;
else else
@ -758,8 +861,21 @@ export default class EmoteMenu extends Module {
return <span key={emote.id} class="emote-picker__placeholder" style={{width: `${emote.width||28}px`, height: `${emote.height||28}px`}} />; return <span key={emote.id} class="emote-picker__placeholder" style={{width: `${emote.width||28}px`, height: `${emote.height||28}px`}} />;
const visibility = this.props.visibility_control, 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; 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 (<button return (<button
key={emote.id} key={emote.id}
class={`ffz-tooltip emote-picker__emote-link${!visibility && locked ? ' locked' : ''}${hidden ? ' emote-hidden' : ''}`} class={`ffz-tooltip emote-picker__emote-link${!visibility && locked ? ' locked' : ''}${hidden ? ' emote-hidden' : ''}`}
@ -775,20 +891,27 @@ export default class EmoteMenu extends Module {
data-locked={emote.locked} data-locked={emote.locked}
data-sellout={sellout} data-sellout={sellout}
onClick={(this.props.visibility_control || !emote.locked) && this.clickEmote} onClick={(this.props.visibility_control || !emote.locked) && this.clickEmote}
onContextMenu={this.contextEmote}
> >
<figure class="emote-picker__emote-figure"> <figure class="emote-picker__emote-figure">
<img <img
class={`emote-picker__emote-image${emote.emoji ? ' ffz-emoji' : ''}`} class={`emote-picker__emote-image${emote.emoji ? ' ffz-emoji' : ''}`}
src={emote.src} src={src}
srcSet={emote.srcSet} srcSet={srcSet}
alt={emote.name} alt={emote.name}
height={emote.height ? `${emote.height}px` : null} height={emote.height ? `${emote.height}px` : null}
width={emote.width ? `${emote.width}px` : null} width={emote.width ? `${emote.width}px` : null}
/> />
</figure> </figure>
{! visibility && has_modifiers && <div class="emote-button__options" />}
{! visibility && emote.favorite && <figure class="ffz--favorite ffz-i-star" />} {! visibility && emote.favorite && <figure class="ffz--favorite ffz-i-star" />}
{! visibility && locked && <figure class="ffz-i-lock" />} {! visibility && locked && <figure class="ffz-i-lock" />}
{hidden && <figure class="ffz-i-eye-off" />} {hidden && <figure class="ffz-i-eye-off" />}
{has_menu && <t.EmoteModifierPicker
emote={emote}
modifiers={modifiers}
close={this.closeEmoteModMenu}
/>}
</button>) </button>)
} }
@ -944,6 +1067,11 @@ export default class EmoteMenu extends Module {
constructor(props) { constructor(props) {
super(props); super(props);
this.nav_ref = null;
this.saveNavRef = ref => {
this.nav_ref = ref;
}
this.ref = null; this.ref = null;
this.saveScrollRef = ref => { this.saveScrollRef = ref => {
this.ref = ref; this.ref = ref;
@ -955,6 +1083,13 @@ export default class EmoteMenu extends Module {
this.state = { this.state = {
tab: null, 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) 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.handleObserve = this.handleObserve.bind(this);
this.pickTone = this.pickTone.bind(this); this.pickTone = this.pickTone.bind(this);
this.clickTab = this.clickTab.bind(this); this.clickTab = this.clickTab.bind(this);
this.clickSideNav = this.clickSideNav.bind(this);
//this.clickRefresh = this.clickRefresh.bind(this); //this.clickRefresh = this.clickRefresh.bind(this);
this.handleFilterChange = this.handleFilterChange.bind(this); this.handleFilterChange = this.handleFilterChange.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this);
@ -1032,22 +1168,39 @@ export default class EmoteMenu extends Module {
this.observer = this._observed = null; this.observer = this._observed = null;
} }
handleObserve(event) { scrollNavIntoView() {
let changed = false; requestAnimationFrame(() => {
const el = this.nav_ref?.querySelector?.(`button[data-key="${this.state.active_nav}"]`);
for(const entry of event) { if ( el )
const inst = this.observing.get(entry.target); el.scrollIntoView({block: 'nearest'});
if ( ! inst || inst.state.intersecting === entry.isIntersecting )
continue;
changed = true;
inst.setState({
intersecting: entry.isIntersecting
}); });
} }
if ( changed ) handleObserve(event) {
let changed = false,
active = this.state.active_nav;
for(const entry of event) {
const inst = this.observing.get(entry.target),
intersecting = entry.isIntersecting;
if ( ! inst || inst.state.intersecting === intersecting )
continue;
changed = true;
inst.setState({intersecting});
if ( intersecting )
active = inst.props?.data?.key;
}
if ( changed ) {
requestAnimationFrame(clearTooltips); requestAnimationFrame(clearTooltips);
if ( ! this.lock_active && active !== this.state.active_nav )
this.setState({
active_nav: active
}, () => this.scrollNavIntoView());
}
} }
startObserving(element, inst) { startObserving(element, inst) {
@ -1078,16 +1231,41 @@ export default class EmoteMenu extends Module {
if ( this.ref ) if ( this.ref )
this.createObserver(); 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; window.ffz_menu = this;
} }
componentWillUnmount() { componentWillUnmount() {
this.destroyObserver(); 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 ) if ( window.ffz_menu === this )
window.ffz_menu = null; 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) { pickTone(tone) {
tone = tone || null; tone = tone || null;
t.settings.provider.set('emoji-tone', tone); 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) { clickTab(event) {
const tab = event.currentTarget.dataset.tab; const tab = event.currentTarget.dataset.tab;
if ( t.chat.context.get('chat.emote-menu.combine-tabs') ) { if ( this.state.combineTabs ) {
let sets; let sets;
switch(tab) { switch(tab) {
case 'fav': case 'fav':
@ -1427,6 +1618,7 @@ export default class EmoteMenu extends Module {
const state = Object.assign({}, old_state), const state = Object.assign({}, old_state),
data = state.set_data || {}, data = state.set_data || {},
modifiers = state.emote_modifiers = {},
channel = state.channel_sets = [], channel = state.channel_sets = [],
all = state.all_sets = [], all = state.all_sets = [],
favorites = state.favorites = []; favorites = state.favorites = [];
@ -1634,6 +1826,8 @@ export default class EmoteMenu extends Module {
section.bad = true; section.bad = true;
} }
let order = 0;
for(const emote of emote_set.emotes) { for(const emote of emote_set.emotes) {
// Validate emotes, because apparently Twitch is handing // Validate emotes, because apparently Twitch is handing
// out bad emote data. // out bad emote data.
@ -1657,6 +1851,9 @@ export default class EmoteMenu extends Module {
srcSet = `${src} 1x, ${base}/2.0 2x` srcSet = `${src} 1x, ${base}/2.0 2x`
} }
/*if ( Array.isArray(emote.modifiers) && emote.modifiers.length )
modifiers[id] = emote.modifiers;*/
const em = { const em = {
provider: 'twitch', provider: 'twitch',
id, id,
@ -1664,6 +1861,7 @@ export default class EmoteMenu extends Module {
name, name,
src, src,
srcSet, srcSet,
order: order++,
overridden: overridden ? mapped.id : null, overridden: overridden ? mapped.id : null,
misc: ! chan, misc: ! chan,
bits: is_bits, bits: is_bits,
@ -1751,6 +1949,8 @@ export default class EmoteMenu extends Module {
else else
section.all_locked = false; section.all_locked = false;
let order = 0;
for(const emote of product.emotes) { for(const emote of product.emotes) {
// Validate emotes, because apparently Twitch is handing // Validate emotes, because apparently Twitch is handing
// out bad emote data. // out bad emote data.
@ -1763,11 +1963,15 @@ export default class EmoteMenu extends Module {
seen = twitch_seen.has(id), seen = twitch_seen.has(id),
is_fav = twitch_favorites.includes(id); is_fav = twitch_favorites.includes(id);
/*if ( Array.isArray(emote.modifiers) && emote.modifiers.length )
modifiers[id] = emote.modifiers;*/
const em = { const em = {
provider: 'twitch', provider: 'twitch',
id, id,
set_id, set_id,
name, name,
order: order++,
locked: locked && ! seen, locked: locked && ! seen,
src: `${base}/1.0`, src: `${base}/1.0`,
srcSet: `${base}/1.0 1x, ${base}/2.0 2x`, srcSet: `${base}/1.0 1x, ${base}/2.0 2x`,
@ -1791,6 +1995,7 @@ export default class EmoteMenu extends Module {
const seen_bits = new Set; const seen_bits = new Set;
if ( Array.isArray(bits) ) { if ( Array.isArray(bits) ) {
let order;
for(const emote of bits) { for(const emote of bits) {
if ( ! emote || ! emote.id || ! emote.bitsBadgeTierSummary ) if ( ! emote || ! emote.id || ! emote.bitsBadgeTierSummary )
continue; continue;
@ -1816,12 +2021,16 @@ export default class EmoteMenu extends Module {
const base = `${TWITCH_EMOTE_BASE}${id}`, const base = `${TWITCH_EMOTE_BASE}${id}`,
is_fav = twitch_favorites.includes(id); is_fav = twitch_favorites.includes(id);
/*if ( Array.isArray(emote.modifiers) && emote.modifiers.length )
modifiers[id] = emote.modifiers;*/
const em = { const em = {
provider: 'twitch', provider: 'twitch',
id, id,
set_id, set_id,
name: emote.token, name: emote.token,
locked, locked,
order: order++,
src: `${base}/1.0`, src: `${base}/1.0`,
srcSet: `${base}/1.0 1x, ${base}/2.0 2x`, srcSet: `${base}/1.0 1x, ${base}/2.0 2x`,
bits: true, bits: true,
@ -1963,8 +2172,10 @@ export default class EmoteMenu extends Module {
provider: 'ffz', provider: 'ffz',
id: emote.id, id: emote.id,
set_id: emote_set.id, set_id: emote_set.id,
src: emote.urls[1], src: emote.src,
srcSet: emote.srcSet, srcSet: emote.srcSet,
animSrc: emote.animSrc,
animSrcSet: emote.animSrcSet,
name: emote.name, name: emote.name,
favorite: is_fav, favorite: is_fav,
hidden: known_hidden.includes(emote.id), hidden: known_hidden.includes(emote.id),
@ -2050,10 +2261,10 @@ export default class EmoteMenu extends Module {
return null; return null;
const loading = this.state.loading || this.props.loading, const loading = this.state.loading || this.props.loading,
padding = t.chat.context.get('chat.emote-menu.reduced-padding'), padding = this.state.reducedPadding, //t.chat.context.get('chat.emote-menu.reduced-padding'),
no_tabs = t.chat.context.get('chat.emote-menu.combine-tabs'); 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 ) { if ( no_tabs ) {
sets = [ sets = [
@ -2069,6 +2280,7 @@ export default class EmoteMenu extends Module {
tab = 'all'; tab = 'all';
is_emoji = tab === 'emoji'; is_emoji = tab === 'emoji';
is_favs = tab === 'fav';
switch(tab) { switch(tab) {
case 'fav': case 'fav':
@ -2097,19 +2309,25 @@ export default class EmoteMenu extends Module {
role="dialog" role="dialog"
> >
<div class="emote-picker"> <div class="emote-picker">
<div class="tw-flex">
<div <div
class="emote-picker__tab-content scrollable-area" class="emote-picker__tab-content tw-full-width scrollable-area scrollable-area--suppress-scroll-x"
data-test-selector="scrollable-area-wrapper" data-test-selector="scrollable-area-wrapper"
data-simplebar data-simplebar
> >
<div ref={this.saveScrollRef} class="simplebar-scroll-content"> <div ref={this.saveScrollRef} class="simplebar-scroll-content">
<div class="simplebar-content"> <div class="simplebar-content">
{loading && this.renderLoading()} {loading && this.renderLoading()}
{!loading && sets && sets.map(data => data && (! visibility || (! data.emoji && ! data.is_favorites)) && createElement( {!loading && sets && sets.map((data,idx) => data && (! visibility || (! data.emoji && ! data.is_favorites)) && createElement(
data.emoji ? t.EmojiSection : t.MenuSection, data.emoji ? t.EmojiSection : t.MenuSection,
{ {
key: data.key, key: data.key,
idx,
data, data,
emote_modifiers: this.state.emote_modifiers,
animated: this.state.animated,
combineTabs: this.state.combineTabs,
showHeading: this.state.showHeading,
filtered: this.state.filtered, filtered: this.state.filtered,
visibility_control: visibility, visibility_control: visibility,
onClickToken: this.props.onClickToken, onClickToken: this.props.onClickToken,
@ -2123,8 +2341,57 @@ export default class EmoteMenu extends Module {
</div> </div>
</div> </div>
</div> </div>
{(! loading && this.state.quickNav && ! is_favs) && (<div class="emote-picker__nav_content tw-block tw-border-radius-none tw-c-background-alt-2">
<div
class="emote-picker__nav-content-overflow scrollable-area scrollable-area--suppress-scroll-x"
data-test-selector="scrollable-area-wrapper"
data-simplebar
>
<div ref={this.saveNavRef} class="simplebar-scroll-content">
<div class="simplebar-content">
{!loading && sets && sets.map(data => {
if ( ! data || (visibility && (data.is_favorites || data.emoji)) )
return null;
const active = this.state.active_nav === data.key;
return (<button
key={data.key}
class={`${active ? 'emote-picker-tab-item-wrapper__active ' : ''}${padding ? 'tw-mg-y-05' : 'tw-mg-y-1'} tw-c-text-inherit tw-interactable ffz-interactive ffz-interactable--hover-enabled ffz-interactable--default tw-block tw-full-width ffz-tooltip ffz-tooltip--no-mouse`}
data-key={data.key}
data-title={`${data.i18n ? t.i18n.t(data.i18n, data.title) : data.title}\n${data.source_i18n ? t.i18n.t(data.source_i18n, data.source) : data.source}`}
data-tooltip-side="left"
onClick={this.clickSideNav}
>
<div class={`tw-align-items-center tw-flex tw-justify-content-center ${padding ? '' : 'tw-pd-x-05 '}tw-pd-y-05${active ? ' emote-picker-tab-item-avatar__active tw-c-text-link' : ''}`}>
{data.image ? <figure class="ffz-avatar ffz-avatar--size-20">
<img
class="tw-block tw-border-radius-rounded tw-img tw-image-avatar"
src={data.image}
srcSet={data.image_set}
/>
</figure> : <figure class={`ffz-emote-picker--nav-icon ffz-i-${data.icon || 'zreknarf'}`} />}
</div>
</button>);
})}
{no_tabs && <div class="tw-mg-y-1 tw-mg-x-05 tw-border-t" />}
{no_tabs && (<button
class="tw-mg-y-1 tw-c-text-inherit tw-interactable ffz-interactive ffz-interactable--hover-enabled ffz-interactable--default tw-block tw-full-width ffz-tooltip ffz-tooltip--no-mouse"
data-title={t.i18n.t('emote-menu.settings', 'Open Settings')}
data-tooltip-side="left"
onClick={this.clickSettings}
>
<div class={`tw-align-items-center tw-flex tw-justify-content-center ${padding ? '' : 'tw-pd-x-05 '}tw-pd-y-05`}>
<figure class="ffz-emote-picker--nav-icon ffz-i-cog" />
</div>
</button>)}
</div>
</div>
</div>
</div>)}
</div>
<div class="emote-picker__controls-container tw-relative"> <div class="emote-picker__controls-container tw-relative">
{(is_emoji || t.chat.context.get('chat.emote-menu.show-search')) && (<div class="tw-border-t tw-pd-1"> {(is_emoji || this.state.showSearch) && (<div class="tw-border-t tw-pd-1">
<div class="tw-flex"> <div class="tw-flex">
<input <input
type="text" type="text"
@ -2141,6 +2408,11 @@ export default class EmoteMenu extends Module {
onChange={this.handleFilterChange} onChange={this.handleFilterChange}
onKeyDown={this.handleKeyDown} onKeyDown={this.handleKeyDown}
/> />
{(no_tabs || is_emoji) && ! visibility && this.state.has_emoji_tab && <t.EmojiTonePicker
tone={this.state.tone}
choices={this.state.tone_emoji}
pickTone={this.pickTone}
/>}
{(no_tabs || ! is_emoji) && <div class="tw-relative tw-tooltip__container tw-mg-l-1"> {(no_tabs || ! is_emoji) && <div class="tw-relative tw-tooltip__container tw-mg-l-1">
<button <button
class={`tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon--primary ffz-core-button tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative${this.state.visibility_control ? ' ffz-core-button--primary' : ' tw-button-icon'}`} class={`tw-align-items-center tw-align-middle tw-border-bottom-left-radius-medium tw-border-bottom-right-radius-medium tw-border-top-left-radius-medium tw-border-top-right-radius-medium tw-button-icon--primary ffz-core-button tw-inline-flex tw-interactive tw-justify-content-center tw-overflow-hidden tw-relative${this.state.visibility_control ? ' ffz-core-button--primary' : ' tw-button-icon'}`}
@ -2160,14 +2432,9 @@ export default class EmoteMenu extends Module {
</div> </div>
</div> </div>
</div>} </div>}
{(no_tabs || is_emoji) && ! visibility && this.state.has_emoji_tab && <t.EmojiTonePicker
tone={this.state.tone}
choices={this.state.tone_emoji}
pickTone={this.pickTone}
/>}
</div> </div>
</div>)} </div>)}
<div class="emote-picker__tab-nav-container tw-flex tw-border-t tw-c-background-alt"> {(no_tabs && this.state.quickNav) ? null : (<div class="emote-picker__tab-nav-container tw-flex tw-border-t tw-c-background-alt">
{! visibility && <div class={`emote-picker-tab-item${tab === 'fav' ? ' emote-picker-tab-item--active' : ''} tw-relative`}> {! visibility && <div class={`emote-picker-tab-item${tab === 'fav' ? ' emote-picker-tab-item--active' : ''} tw-relative`}>
<button <button
class={`ffz-tooltip tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive${tab === 'fav' ? ' ffz-interactable--selected' : ''}`} class={`ffz-tooltip tw-block tw-full-width ffz-interactable ffz-interactable--hover-enabled ffz-interactable--default tw-interactive${tab === 'fav' ? ' ffz-interactable--selected' : ''}`}
@ -2237,7 +2504,7 @@ export default class EmoteMenu extends Module {
</div> </div>
</button> </button>
</div> </div>
</div> </div>)}
</div> </div>
</div> </div>
</div> </div>

View file

@ -61,6 +61,7 @@ export default class ChatLine extends Module {
this.on('i18n:update', this.updateLines, this); 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.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.emoji.style', this.updateLines, this);
this.chat.context.on('changed:chat.bits.stack', 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.style', this.updateLines, this);
@ -337,6 +338,7 @@ export default class ChatLine extends Module {
const types = t.parent.message_types || {}, const types = t.parent.message_types || {},
deleted_count = this.props.deletedCount, deleted_count = this.props.deletedCount,
reply_mode = t.chat.context.get('chat.replies.style'), 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'), override_mode = t.chat.context.get('chat.filtering.display-deleted'),
msg = t.chat.standardizeMessage(this.props.message), msg = t.chat.standardizeMessage(this.props.message),
@ -892,6 +894,8 @@ other {# messages were deleted by a moderator.}
'data-room': room, 'data-room': room,
'data-user-id': user.userID, 'data-user-id': user.userID,
'data-user': user.userLogin && user.userLogin.toLowerCase(), 'data-user': user.userLogin && user.userLogin.toLowerCase(),
onMouseOver: anim_hover ? t.chat.emotes.animHover : null,
onMouseOut: anim_hover ? t.chat.emotes.animLeave : null
}, out); }, out);
} catch(err) { } catch(err) {

View file

@ -117,10 +117,15 @@
transform: rotate(90deg); transform: rotate(90deg);
} }
.emote-picker__nav-content-overflow,
.emote-picker__tab-content { .emote-picker__tab-content {
max-height: calc(var(--ffz-chat-height) - 26rem); max-height: calc(var(--ffz-chat-height) - 26rem);
} }
.emote-picker__nav-content-overflow {
height: 100% !important;
}
&.right-column--theatre { &.right-column--theatre {
.ffz--portrait-invert & { .ffz--portrait-invert & {
bottom: unset !important; bottom: unset !important;
@ -132,6 +137,7 @@
height: calc(100vh - var(--ffz-theater-height)) !important; height: calc(100vh - var(--ffz-theater-height)) !important;
.emote-picker__nav-content-overflow,
.emote-picker__tab-content { .emote-picker__tab-content {
max-height: calc(calc(100vh - var(--ffz-theater-height)) - 26rem); max-height: calc(calc(100vh - var(--ffz-theater-height)) - 26rem);
} }

View file

@ -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 { .ffz--emote-picker {
section:not(.filtered) heading { section:not(.filtered) heading {
cursor: pointer; cursor: pointer;
@ -311,6 +330,11 @@
} }
@media only screen and (max-height: 750px) { @media only screen and (max-height: 750px) {
.emote-picker__nav-content-overflow {
height: 100% !important;
}
.emote-picker__nav-content-overflow,
.emote-picker__tab-content { .emote-picker__tab-content {
#root & { #root & {
max-height: calc(100vh - 31rem); max-height: calc(100vh - 31rem);

View file

@ -12,6 +12,7 @@
.clmgr-table__row, .clmgr-table__row,
.sunlight-expanded-nav-drop-down-menu-layout__scrollable-area, .sunlight-expanded-nav-drop-down-menu-layout__scrollable-area,
.stream-manager--page-view .mosaic-window-body, .stream-manager--page-view .mosaic-window-body,
.emote-grid-section__header-title,
.ach-card, .ach-card,
.ach-card--expanded .ach-card__inner, .ach-card--expanded .ach-card__inner,
.room-upsell, .room-upsell,

View file

@ -6,18 +6,20 @@
z-index: 99999999; z-index: 99999999;
height: 50vh; --width: #{"min(75vw, 128rem)"};
width: 50vw; --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-width: 64rem;
min-height: 30rem; min-height: 30rem;
--width: #{"min(75vw, 128rem)"};
width: 75vw; width: 75vw;
width: var(--width); width: var(--width);
height: 50vh; height: 50vh;
height: #{"min(75vh, calc(0.75 * var(--width)))"}; height: var(--height);
> header { > header {
cursor: move; cursor: move;

View file

@ -221,12 +221,20 @@
max-width: 100%; max-width: 100%;
width: auto; width: auto;
} }
}
.ffz-textarea--error:focus { &--error {
box-shadow: var(--shadow-input-error);
&,&:focus {
border: var(--border-width-input) solid var(--color-border-input-error);
}
&:focus {
box-shadow: var(--shadow-input-error-focus); box-shadow: var(--shadow-input-error-focus);
} }
}
.ffz-textarea--no-resize { &--no-resize {
resize: none; resize: none;
}
} }