From 43832890b8fa9b7cf1c4c4ae8a8df41be84239a8 Mon Sep 17 00:00:00 2001 From: SirStendec Date: Thu, 26 Jul 2018 19:40:53 -0400 Subject: [PATCH] 4.0.0-rc9 * Added: Emoji to the Emote Menu. * Changed: Do not look up all a user's emote sets until they actually open the emote menu to reduce server load. * Changed: Ignore a few extra useless errors with automatic error reporting. * Fixed: Adding the Prime icon to Subscribe buttons when your free Prime sub is available. * Fixed: Uncaught exceptions when a pop-up blocker stops us from opening a new tab. --- src/main.js | 2 +- src/modules/chat/actions/types.jsx | 6 +- src/modules/chat/emotes.js | 46 +- src/raven.js | 4 +- .../modules/chat/emote_menu.jsx | 441 ++++++++++++++---- .../modules/chat/tab_completion.jsx | 14 +- src/sites/twitch-twilight/modules/player.jsx | 2 +- .../twitch-twilight/modules/sub_button.jsx | 10 +- src/sites/twitch-twilight/styles/chat.scss | 25 + styles/icons.scss | 4 +- 10 files changed, 434 insertions(+), 120 deletions(-) diff --git a/src/main.js b/src/main.js index 05a4bd48..4973678e 100644 --- a/src/main.js +++ b/src/main.js @@ -100,7 +100,7 @@ class FrankerFaceZ extends Module { FrankerFaceZ.Logger = Logger; const VER = FrankerFaceZ.version_info = { - major: 4, minor: 0, revision: 0, extra: '-rc8.6.1', + major: 4, minor: 0, revision: 0, extra: '-rc9', commit: __git_commit__, build: __webpack_hash__, toString: () => diff --git a/src/modules/chat/actions/types.jsx b/src/modules/chat/actions/types.jsx index d7f2296e..163a99de 100644 --- a/src/modules/chat/actions/types.jsx +++ b/src/modules/chat/actions/types.jsx @@ -46,8 +46,10 @@ export const open_url = { const url = process(data.options.url, data, this.i18n.locale); const win = window.open(); - win.opener = null; - win.location = url; + if ( win ) { + win.opener = null; + win.location = url; + } } }; diff --git a/src/modules/chat/emotes.js b/src/modules/chat/emotes.js index 8cdc4b8e..28c3eb7c 100644 --- a/src/modules/chat/emotes.js +++ b/src/modules/chat/emotes.js @@ -225,8 +225,10 @@ export default class Emotes extends Module { if ( url ) { const win = window.open(); - win.opener = null; - win.location = url; + if ( win ) { + win.opener = null; + win.location = url; + } } return true; @@ -723,14 +725,50 @@ export default class Emotes extends Module { } - getTwitchSetChannel(set_id, callback) { - const tes = this.__twitch_set_to_channel; + async awaitTwitchSetChannel(set_id, perform_lookup = true) { + const tes = this.__twitch_set_to_channel, + inv = this.twitch_inventory_sets; + if ( isNaN(set_id) || ! isFinite(set_id) ) return null; if ( tes.has(set_id) ) return tes.get(set_id); + if ( inv.has(set_id) ) + return {s_id: set_id, c_id: null, c_name: 'twitch-inventory'} + + if ( ! perform_lookup ) + return null; + + tes.set(set_id, null); + try { + const data = await timeout(this.socket.call('get_emote_set', set_id), 1000); + tes.set(set_id, data); + return data; + + } catch(err) { + tes.delete(set_id); + } + } + + + getTwitchSetChannel(set_id, callback, perform_lookup = true) { + const tes = this.__twitch_set_to_channel, + inv = this.twitch_inventory_sets; + + if ( isNaN(set_id) || ! isFinite(set_id) ) + return null; + + if ( tes.has(set_id) ) + return tes.get(set_id); + + if ( inv.has(set_id) ) + return {s_id: set_id, c_id: null, c_name: 'twitch-inventory'} + + if ( ! perform_lookup ) + return null; + tes.set(set_id, null); timeout(this.socket.call('get_emote_set', set_id), 1000).then(data => { tes.set(set_id, data); diff --git a/src/raven.js b/src/raven.js index 89a55cf6..5725478f 100644 --- a/src/raven.js +++ b/src/raven.js @@ -130,7 +130,9 @@ export default class RavenLogger extends Module { captureUnhandledRejections: false, ignoreErrors: [ 'InvalidAccessError', - 'out of memory' + 'out of memory', + 'Access is denied.', + 'Zugriff verweigert' ], whitelistUrls: [ /cdn\.frankerfacez\.com/ diff --git a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx index cd2698a5..681708d9 100644 --- a/src/sites/twitch-twilight/modules/chat/emote_menu.jsx +++ b/src/sites/twitch-twilight/modules/chat/emote_menu.jsx @@ -5,13 +5,23 @@ // ============================================================================ import {has, get, once, maybe_call, set_equals} from 'utilities/object'; -import {IS_OSX, KNOWN_CODES, TWITCH_EMOTE_BASE, REPLACEMENT_BASE, REPLACEMENTS} from 'utilities/constants'; +import {WEBKIT_CSS as WEBKIT, IS_OSX, KNOWN_CODES, TWITCH_EMOTE_BASE, REPLACEMENT_BASE, REPLACEMENTS} from 'utilities/constants'; +import {ClickOutside} from 'utilities/dom'; import Twilight from 'site'; import Module from 'utilities/module'; import SUB_STATUS from './sub_status.gql'; +const TONE_EMOJI = [ + 'the_horns', + 'raised_back_of_hand', + 'ok_hand', + '+1', + 'clap', + 'fist' +]; + function maybe_date(val) { if ( ! val ) return val; @@ -162,7 +172,8 @@ export default class EmoteMenu extends Module { data: [ {value: 'fav', title: 'Favorites'}, {value: 'channel', title: 'Channel'}, - {value: 'all', title: 'My Emotes'} + {value: 'all', title: 'My Emotes'}, + {value: 'emoji', title: 'Emoji'} ] } }); @@ -230,11 +241,19 @@ export default class EmoteMenu extends Module { 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.emoji.style', this.updateEmojiVariables, this); + this.chat.context.on('changed:chat.emote-menu.icon', val => this.css_tweaks.toggle('emote-menu', val)); this.css_tweaks.toggle('emote-menu', this.chat.context.get('chat.emote-menu.icon')); + this.updateEmojiVariables(); + + this.css_tweaks.setVariable('emoji-menu--sheet', `//cdn.frankerfacez.com/static/emoji/sheet_twitter_32.png`); + this.css_tweaks.setVariable('emoji-menu--count', 52); + this.css_tweaks.setVariable('emoji-menu--size', 20); + const t = this, React = await this.web_munch.findModule('react'), createElement = React && React.createElement; @@ -269,6 +288,28 @@ export default class EmoteMenu extends Module { }) } + updateEmojiVariables() { + const style = this.chat.context.get('chat.emoji.style') || 'twitter', + base = `//cdn.frankerfacez.com/static/emoji/sheet_${style}_`; + + const emoji_size = this.emoji_size = 20, + sheet_count = this.emoji_sheet_count = 52, + sheet_size = this.emoji_sheet_size = sheet_count * (emoji_size + 2), + sheet_pct = this.emoji_sheet_pct = 100 * sheet_size / emoji_size; + + this.emoji_sheet_remain = sheet_size - emoji_size; + + this.css_tweaks.set('emoji-menu', `.ffz--emoji-tone-picker__emoji,.emote-picker__emoji .emote-picker__emote-figure { + background-size: ${sheet_pct}% ${sheet_pct}%; + background-image: url("${base}20.png"); + background-image: ${WEBKIT}image-set( + url("${base}20.png") 1x, + url("${base}32.png") 1.6x, + url("${base}64.png") 3.2x + ); +}`); + } + maybeUpdate() { if ( this.chat.context.get('chat.emote-menu.enabled') ) this.EmoteMenu.forceUpdate(); @@ -289,49 +330,112 @@ export default class EmoteMenu extends Module { React = this.web_munch.getModule('react'), createElement = React && React.createElement; - this.MenuEmote = function({source, data, lock, locked, all_locked, onClickEmote}) { - const handle_click = e => { - if ( ! t.emotes.handleClick(e) ) - onClickEmote(data.name); - }; + this.EmojiTonePicker = class FFZEmojiTonePicker extends React.Component { + constructor(props) { + super(props); - const sellout = lock ? - all_locked ? - t.i18n.t('emote-menu.emote-sub', 'Subscribe for %{price} to unlock this emote.', lock) : - t.i18n.t('emote-menu.emote-up', 'Upgrade your sub to %{price} to unlock this emote.', lock) - : null; + this.onClick = () => this.setState({open: ! this.state.open}); + this.onMouseEnter = () => this.state.open || this.setState({emoji: this.pickRandomEmoji()}); + this.onClickOutside = () => this.state.open && this.setState({open: false}); - return () + this.clickTone = event => { + this.props.pickTone(event.currentTarget.dataset.tone); + this.setState({open: false}); + } + + this.element = null; + this.saveRef = element => this.element = element; + + this.state = { + open: false, + emoji: this.pickRandomEmoji(), + tone: null + } + } + + componentDidMount() { + if ( this.element ) + this._clicker = new ClickOutside(this.element, this.onClickOutside); + } + + componentWillUnmount() { + if ( this._clicker ) { + this._clicker.destroy(); + this._clicker = null; + } + } + + pickRandomEmoji() { // eslint-disable-line class-methods-use-this + const possibilities = this.props.choices, + pick = Math.floor(Math.random() * possibilities.length); + + return possibilities[pick]; + } + + renderTone(data, tone) { + return () + } + + renderToneMenu() { + if ( ! this.state.open ) + return null; + + const emoji = this.state.emoji, + tones = Object.entries(emoji.variants).map(([tone, emoji]) => this.renderTone(emoji, tone)); + + return (
+
+ {this.renderTone(emoji, null)} + {tones} +
+
); + } + + renderEmoji(data) { // eslint-disable-line class-methods-use-this + const emoji_x = (data.sheet_x * (t.emoji_size + 2)) + 1, + emoji_y = (data.sheet_y * (t.emoji_size + 2)) + 1, + + x_pct = 100 * emoji_x / t.emoji_sheet_remain, + y_pct = 100 * emoji_y / t.emoji_sheet_remain; + + return (
) + } + + render() { + const emoji = this.state.emoji, + tone = this.props.tone, + toned = tone && emoji.variants[tone]; + + return (
+ + {this.renderToneMenu()} +
) + } } - this.MenuSection = class FFZMenuSection extends React.Component { constructor(props) { super(props); @@ -340,10 +444,19 @@ export default class EmoteMenu extends Module { this.state = {collapsed: props.data && collapsed.includes(props.data.key)} this.clickHeading = this.clickHeading.bind(this); + this.clickEmote = this.clickEmote.bind(this); + this.onMouseEnter = this.onMouseEnter.bind(this); this.onMouseLeave = this.onMouseLeave.bind(this); } + clickEmote(event) { + if ( t.emotes.handleClick(event) ) + return; + + this.props.onClickEmote(event.currentTarget.dataset.name) + } + clickHeading() { if ( this.props.filtered ) return; @@ -444,15 +557,25 @@ export default class EmoteMenu extends Module { const data = this.props.data, filtered = this.props.filtered, lock = data.locks && data.locks[this.state.unlocked], - emotes = data.filtered_emotes && data.filtered_emotes.map(emote => (! filtered || ! emote.locked) && ()); + + emotes = data.filtered_emotes && data.filtered_emotes.map(emote => { + if ( filtered && emote.locked ) + return; + + const locked = emote.locked && (! lock || ! lock.emotes.has(emote.id)), + emote_lock = locked && data.locks && data.locks[emote.set_id], + sellout = emote_lock && (data.all_locked ? + t.i18n.t('emote-menu.emote-sub', 'Subscribe for %{price} to unlock this emote.', emote_lock) : + t.i18n.t('emote-menu.emote-up', 'Upgrade your sub to %{price} to unlock this emote.', emote_lock) + ); + + return this.renderEmote( + emote, + locked, + show_sources, + sellout + ); + }); return (
{emotes} @@ -460,6 +583,38 @@ export default class EmoteMenu extends Module {
) } + renderEmote(emote, locked, source, sellout) { + return () + } + renderSellout() { const data = this.props.data; @@ -475,7 +630,7 @@ export default class EmoteMenu extends Module {
{Object.values(data.locks).map(lock => ( +
+ {emote.favorite &&
} + {locked &&
} + ) + } + } + this.MenuComponent = class FFZEmoteMenuComponent extends React.Component { constructor(props) { super(props); - this.state = {tab: null} + this.state = { + tab: null, + tone: t.settings.provider.get('emoji-tone', null) + } + this.componentWillReceiveProps(props); + this.pickTone = this.pickTone.bind(this); this.clickTab = this.clickTab.bind(this); this.clickRefresh = this.clickRefresh.bind(this); this.handleFilterChange = this.handleFilterChange.bind(this); @@ -517,6 +713,17 @@ export default class EmoteMenu extends Module { window.ffz_menu = null; } + pickTone(tone) { + t.settings.provider.set('emoji-tone', tone); + + this.setState(this.filterState( + this.state.filter, + this.buildEmoji( + Object.assign({}, this.state, {tone}) + ) + )); + } + clickTab(event) { this.setState({ tab: event.target.dataset.tab @@ -583,27 +790,24 @@ export default class EmoteMenu extends Module { this.setState({loading: true}, () => { t.getData(sets, force).then(d => { - this.setState(this.filterState(this.state.filter, this.buildState( - this.props, - Object.assign({}, this.state, {set_sets: sets, set_data: d, loading: false}) - ))); + const promises = []; + + for(const set_id of sets) + if ( ! has(d, set_id) ) + promises.push(t.emotes.awaitTwitchSetChannel(set_id)) + + Promise.all(promises).then(() => { + this.setState(this.filterState(this.state.filter, this.buildState( + this.props, + Object.assign({}, this.state, {set_sets: sets, set_data: d, loading: false}) + ))); + }); }); }); return true; } - loadSets(props) { // eslint-disable-line class-methods-use-this - const emote_sets = props.emote_data && props.emote_data.emoteSets; - if ( ! emote_sets || ! emote_sets.length ) - return; - - for(const emote_set of emote_sets) { - const set_id = parseInt(emote_set.id, 10); - t.emotes.getTwitchSetChannel(set_id) - } - } - filterState(input, old_state) { const state = Object.assign({}, old_state); @@ -657,26 +861,36 @@ export default class EmoteMenu extends Module { buildEmoji(old_state) { // eslint-disable-line class-methods-use-this - return old_state; - - /*const state = Object.assign({}, old_state), + const state = Object.assign({}, old_state), sets = state.emoji_sets = [], emoji_favorites = t.emotes.getFavorites('emoji'), style = t.chat.context.get('chat.emoji.style') || 'twitter', - favorites = state.favorites = state.favorites || [], + favorites = state.favorites = (state.favorites || []).filter(x => ! x.emoji), + + tone = state.tone = state.tone || null, + tone_choices = state.tone_emoji = [], categories = {}; for(const emoji of Object.values(t.emoji.emoji)) { - if ( ! emoji.has[style] ) + if ( ! emoji.has[style] || emoji.category === 'Skin Tones' ) continue; + if ( emoji.variants ) { + for(const name of emoji.names) + if ( TONE_EMOJI.includes(name) ) { + tone_choices.push(emoji); + break; + } + } + let cat = categories[emoji.category]; if ( ! cat ) { cat = categories[emoji.category] = []; sets.push({ key: `emoji-${emoji.category}`, + emoji: true, image: t.emoji.getFullImage(emoji.image), title: emoji.category, source: t.i18n.t('emote-menu.emoji', 'Emoji'), @@ -685,24 +899,29 @@ export default class EmoteMenu extends Module { } const is_fav = emoji_favorites.includes(emoji.code), + toned = emoji.variants && emoji.variants[tone], + has_tone = toned && toned.has[style], + source = has_tone ? toned : emoji, + em = { provider: 'emoji', emoji: true, code: emoji.code, - name: emoji.raw, + name: source.raw, + variant: has_tone && tone, search: emoji.names[0], height: 18, width: 18, - x: emoji.sheet_x, - y: emoji.sheet_y, + x: source.sheet_x, + y: source.sheet_y, favorite: is_fav, - src: t.emoji.getFullImage(emoji.image), - srcSet: t.emoji.getFullImageSet(emoji.image) + src: t.emoji.getFullImage(source.image), + srcSet: t.emoji.getFullImageSet(source.image) }; cat.push(em); @@ -713,10 +932,23 @@ export default class EmoteMenu extends Module { state.has_emoji_tab = sets.length > 0; - return state;*/ + state.fav_sets = [{ + key: 'favorites', + is_favorites: true, + emotes: favorites + }]; + + // We use this sorter because we don't want things grouped by sets. + favorites.sort(this.getSorter()); + + return state; } + getSorter() { // eslint-disable-line class-methods-use-this + return EMOTE_SORTERS[t.chat.context.get('chat.emote-menu.sort-emotes')]; + } + buildState(props, old_state) { const state = Object.assign({}, old_state), @@ -735,7 +967,7 @@ export default class EmoteMenu extends Module { return state; // Sorters - const sorter = EMOTE_SORTERS[t.chat.context.get('chat.emote-menu.sort-emotes')], + const sorter = this.getSorter(), sort_tiers = t.chat.context.get('chat.emote-menu.sort-tiers-last'), sort_emotes = (a,b) => { if ( a.inventory || b.inventory ) @@ -752,6 +984,7 @@ export default class EmoteMenu extends Module { return sorter(a,b); } + // Start with the All tab. Some data calculated for // all is re-used for the Channel tab. @@ -772,7 +1005,7 @@ export default class EmoteMenu extends Module { const set_id = parseInt(emote_set.id, 10), is_inventory = inventory.has(set_id), set_data = data[set_id] || {}, - more_data = t.emotes.getTwitchSetChannel(set_id), + more_data = t.emotes.getTwitchSetChannel(set_id, null, false), image = set_data.image, image_set = set_data.image_set; @@ -802,6 +1035,11 @@ export default class EmoteMenu extends Module { icon = 'inventory'; sort_key = 50; + } else if ( set_data && set_data.type === 'turbo' ) { + title = t.i18n.t('emote-menu.prime', 'Prime'); + icon = 'crown'; + sort_key = 75; + } else if ( more_data ) { title = more_data.c_name; @@ -1028,15 +1266,6 @@ export default class EmoteMenu extends Module { state.has_channel_tab = channel.length > 0; - state.fav_sets = [{ - key: 'favorites', - is_favorites: true, - emotes: favorites - }]; - - // We use this sorter because we don't want things grouped by sets. - favorites.sort(sorter); - return this.buildEmoji(state); } @@ -1125,8 +1354,6 @@ export default class EmoteMenu extends Module { if ( props.visible ) this.loadData(); - this.loadSets(props); - const state = this.buildState(props, this.state); this.setState(this.filterState(state.filter, state)); } @@ -1184,6 +1411,8 @@ export default class EmoteMenu extends Module { if ( (tab === 'channel' && ! this.state.has_channel_tab) || (tab === 'emoji' && ! this.state.has_emoji_tab) ) tab = 'all'; + const is_emoji = tab === 'emoji'; + switch(tab) { case 'fav': sets = this.state.filtered_fav_sets; @@ -1213,30 +1442,42 @@ export default class EmoteMenu extends Module {
{loading && this.renderLoading()} - {!loading && sets && sets.map(data => ())} + {!loading && sets && sets.map(data => createElement( + data.emoji ? t.EmojiSection : t.MenuSection, + { + key: data.key, + data, + filtered: this.state.filtered, + onClickEmote: this.props.onClickEmote + } + ))} {! loading && (! sets || ! sets.length) && this.renderEmpty()}
- {t.chat.context.get('chat.emote-menu.show-search') && (
-
+ {(is_emoji || t.chat.context.get('chat.emote-menu.show-search')) && (
+
+ {is_emoji && }
)}
diff --git a/src/sites/twitch-twilight/modules/chat/tab_completion.jsx b/src/sites/twitch-twilight/modules/chat/tab_completion.jsx index c62e9b56..db940ebc 100644 --- a/src/sites/twitch-twilight/modules/chat/tab_completion.jsx +++ b/src/sites/twitch-twilight/modules/chat/tab_completion.jsx @@ -147,6 +147,7 @@ export default class TabCompletion extends Module { getEmojiSuggestions(input, inst) { let search = input.slice(1).toLowerCase(); const style = this.chat.context.get('chat.emoji.style'), + tone = this.settings.provider.get('emoji-tone', null), results = [], has_colon = search.endsWith(':'); @@ -155,16 +156,19 @@ export default class TabCompletion extends Module { for(const name in this.emoji.names) if ( has_colon ? name === search : name.startsWith(search) ) { - const emoji = this.emoji.emoji[this.emoji.names[name]]; - if ( emoji && (style === 0 || emoji.has[style]) ) + const emoji = this.emoji.emoji[this.emoji.names[name]], + toned = emoji.variants && emoji.variants[tone], + source = toned || emoji; + + if ( emoji && (style === 0 || source.has[style]) ) results.push({ current: input, - replacement: emoji.raw, + replacement: source.raw, element: inst.renderFFZEmojiSuggestion({ token: `:${name}:`, id: `emoji-${emoji.code}`, - src: this.emoji.getFullImage(emoji.image, style), - srcSet: this.emoji.getFullImageSet(emoji.image, style) + src: this.emoji.getFullImage(source.image, style), + srcSet: this.emoji.getFullImageSet(source.image, style) }) }); } diff --git a/src/sites/twitch-twilight/modules/player.jsx b/src/sites/twitch-twilight/modules/player.jsx index 6015af48..1b46ffed 100644 --- a/src/sites/twitch-twilight/modules/player.jsx +++ b/src/sites/twitch-twilight/modules/player.jsx @@ -29,7 +29,7 @@ export default class Player extends Module { this.PersistentPlayer = this.fine.define( 'twitch-player-persistent', - n => n.renderMiniControl && n.renderMiniTitle && n.handleWindowResize, + n => n.renderMiniHoverControls && n.togglePause, ['front-page', 'user', 'video', 'dash'] ); diff --git a/src/sites/twitch-twilight/modules/sub_button.jsx b/src/sites/twitch-twilight/modules/sub_button.jsx index 1bc0f2b3..9a697e43 100644 --- a/src/sites/twitch-twilight/modules/sub_button.jsx +++ b/src/sites/twitch-twilight/modules/sub_button.jsx @@ -32,13 +32,16 @@ export default class SubButton extends Module { this.SubButton = this.fine.define( 'sub-button', - n => n.reportSubMenuAction && n.isUserDataReady, + n => n.handleSubMenuAction && n.isUserDataReady, ['user', 'video'] ); } onEnable() { - this.SubButton.ready(() => this.SubButton.forceUpdate()); + this.SubButton.ready((cls, instances) => { + for(const inst of instances) + this.updateSubButton(inst); + }); this.SubButton.on('mount', this.updateSubButton, this); this.SubButton.on('update', this.updateSubButton, this); @@ -47,7 +50,7 @@ export default class SubButton extends Module { updateSubButton(inst) { const container = this.fine.getChildNode(inst), - btn = container && container.querySelector('button[data-test-selector="subscribe-button__dropdown"]'); + btn = container && container.querySelector('button.tw-button--dropmenu'); if ( ! btn ) return; @@ -62,7 +65,6 @@ export default class SubButton extends Module { btn.insertBefore(
, btn.firstElementChild); diff --git a/src/sites/twitch-twilight/styles/chat.scss b/src/sites/twitch-twilight/styles/chat.scss index 3973f35a..ffa7ef0d 100644 --- a/src/sites/twitch-twilight/styles/chat.scss +++ b/src/sites/twitch-twilight/styles/chat.scss @@ -176,6 +176,22 @@ } } + .ffz--emoji-tone-picker { + .tw-balloon { + min-width: unset; + } + + .tw-button__text { + padding: .2rem .4rem; + padding-right: .8rem; + } + } + + .ffz--emoji-tone-picker__emoji { + width: 2rem; + height: 2rem; + } + .emote-picker__emote-link { position: relative; padding: 0.5rem; @@ -187,6 +203,15 @@ vertical-align: middle; } + &.emote-picker__emoji { + min-width: unset; + + .emote-picker__emote-figure { + width: 2rem; + height: 2rem; + } + } + &.locked { cursor: not-allowed; diff --git a/styles/icons.scss b/styles/icons.scss index 36b89ba9..e110b6a5 100644 --- a/styles/icons.scss +++ b/styles/icons.scss @@ -57,9 +57,9 @@ .tw-button__icon .ffz-i-crown:before { - margin: 0; font-size: 1.6rem; - vertical-align: sub; + vertical-align: middle; + margin-bottom: -.6rem; } .ffz-i-cancel:before { content: '\e800'; } /* '' */