mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-06 14:20:56 +00:00
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.
This commit is contained in:
parent
2869eaedd8
commit
43832890b8
10 changed files with 434 additions and 120 deletions
|
@ -100,7 +100,7 @@ class FrankerFaceZ extends Module {
|
||||||
FrankerFaceZ.Logger = Logger;
|
FrankerFaceZ.Logger = Logger;
|
||||||
|
|
||||||
const VER = FrankerFaceZ.version_info = {
|
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__,
|
commit: __git_commit__,
|
||||||
build: __webpack_hash__,
|
build: __webpack_hash__,
|
||||||
toString: () =>
|
toString: () =>
|
||||||
|
|
|
@ -46,9 +46,11 @@ export const open_url = {
|
||||||
const url = process(data.options.url, data, this.i18n.locale);
|
const url = process(data.options.url, data, this.i18n.locale);
|
||||||
|
|
||||||
const win = window.open();
|
const win = window.open();
|
||||||
|
if ( win ) {
|
||||||
win.opener = null;
|
win.opener = null;
|
||||||
win.location = url;
|
win.location = url;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -225,9 +225,11 @@ export default class Emotes extends Module {
|
||||||
|
|
||||||
if ( url ) {
|
if ( url ) {
|
||||||
const win = window.open();
|
const win = window.open();
|
||||||
|
if ( win ) {
|
||||||
win.opener = null;
|
win.opener = null;
|
||||||
win.location = url;
|
win.location = url;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -723,14 +725,50 @@ export default class Emotes extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
getTwitchSetChannel(set_id, callback) {
|
async awaitTwitchSetChannel(set_id, perform_lookup = true) {
|
||||||
const tes = this.__twitch_set_to_channel;
|
const tes = this.__twitch_set_to_channel,
|
||||||
|
inv = this.twitch_inventory_sets;
|
||||||
|
|
||||||
if ( isNaN(set_id) || ! isFinite(set_id) )
|
if ( isNaN(set_id) || ! isFinite(set_id) )
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
if ( tes.has(set_id) )
|
if ( tes.has(set_id) )
|
||||||
return tes.get(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);
|
tes.set(set_id, null);
|
||||||
timeout(this.socket.call('get_emote_set', set_id), 1000).then(data => {
|
timeout(this.socket.call('get_emote_set', set_id), 1000).then(data => {
|
||||||
tes.set(set_id, data);
|
tes.set(set_id, data);
|
||||||
|
|
|
@ -130,7 +130,9 @@ export default class RavenLogger extends Module {
|
||||||
captureUnhandledRejections: false,
|
captureUnhandledRejections: false,
|
||||||
ignoreErrors: [
|
ignoreErrors: [
|
||||||
'InvalidAccessError',
|
'InvalidAccessError',
|
||||||
'out of memory'
|
'out of memory',
|
||||||
|
'Access is denied.',
|
||||||
|
'Zugriff verweigert'
|
||||||
],
|
],
|
||||||
whitelistUrls: [
|
whitelistUrls: [
|
||||||
/cdn\.frankerfacez\.com/
|
/cdn\.frankerfacez\.com/
|
||||||
|
|
|
@ -5,13 +5,23 @@
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
import {has, get, once, maybe_call, set_equals} from 'utilities/object';
|
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 Twilight from 'site';
|
||||||
import Module from 'utilities/module';
|
import Module from 'utilities/module';
|
||||||
|
|
||||||
import SUB_STATUS from './sub_status.gql';
|
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) {
|
function maybe_date(val) {
|
||||||
if ( ! val )
|
if ( ! val )
|
||||||
return val;
|
return val;
|
||||||
|
@ -162,7 +172,8 @@ export default class EmoteMenu extends Module {
|
||||||
data: [
|
data: [
|
||||||
{value: 'fav', title: 'Favorites'},
|
{value: 'fav', title: 'Favorites'},
|
||||||
{value: 'channel', title: 'Channel'},
|
{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.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.emoji.style', this.updateEmojiVariables, this);
|
||||||
|
|
||||||
this.chat.context.on('changed:chat.emote-menu.icon', val =>
|
this.chat.context.on('changed:chat.emote-menu.icon', val =>
|
||||||
this.css_tweaks.toggle('emote-menu', val));
|
this.css_tweaks.toggle('emote-menu', val));
|
||||||
|
|
||||||
this.css_tweaks.toggle('emote-menu', this.chat.context.get('chat.emote-menu.icon'));
|
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,
|
const t = this,
|
||||||
React = await this.web_munch.findModule('react'),
|
React = await this.web_munch.findModule('react'),
|
||||||
createElement = React && React.createElement;
|
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() {
|
maybeUpdate() {
|
||||||
if ( this.chat.context.get('chat.emote-menu.enabled') )
|
if ( this.chat.context.get('chat.emote-menu.enabled') )
|
||||||
this.EmoteMenu.forceUpdate();
|
this.EmoteMenu.forceUpdate();
|
||||||
|
@ -289,48 +330,111 @@ 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.MenuEmote = function({source, data, lock, locked, all_locked, onClickEmote}) {
|
this.EmojiTonePicker = class FFZEmojiTonePicker extends React.Component {
|
||||||
const handle_click = e => {
|
constructor(props) {
|
||||||
if ( ! t.emotes.handleClick(e) )
|
super(props);
|
||||||
onClickEmote(data.name);
|
|
||||||
};
|
|
||||||
|
|
||||||
const sellout = lock ?
|
this.onClick = () => this.setState({open: ! this.state.open});
|
||||||
all_locked ?
|
this.onMouseEnter = () => this.state.open || this.setState({emoji: this.pickRandomEmoji()});
|
||||||
t.i18n.t('emote-menu.emote-sub', 'Subscribe for %{price} to unlock this emote.', lock) :
|
this.onClickOutside = () => this.state.open && this.setState({open: false});
|
||||||
t.i18n.t('emote-menu.emote-up', 'Upgrade your sub to %{price} to unlock this emote.', lock)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
|
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 (<button
|
return (<button
|
||||||
class={`ffz-tooltip emote-picker__emote-link${locked ? ' locked' : ''}`}
|
key={data.code}
|
||||||
data-tooltip-type="emote"
|
data-tone={tone}
|
||||||
data-provider={data.provider}
|
class="tw-interactive tw-block tw-full-width tw-interactable tw-interactable--inverted tw-pd-y-05 tw-pd-x-2"
|
||||||
data-id={data.id}
|
onClick={this.clickTone}
|
||||||
data-set={data.set_id}
|
|
||||||
data-code={data.code}
|
|
||||||
data-variant={data.variant}
|
|
||||||
data-no-source={source}
|
|
||||||
data-name={data.name}
|
|
||||||
aria-label={data.name}
|
|
||||||
data-locked={data.locked}
|
|
||||||
data-sellout={sellout}
|
|
||||||
onClick={!data.locked && handle_click}
|
|
||||||
>
|
>
|
||||||
<figure class="emote-picker__emote-figure">
|
{this.renderEmoji(data)}
|
||||||
<img
|
|
||||||
class={`emote-picker__emote-image${data.emoji ? ' ffz-emoji' : ''}`}
|
|
||||||
src={data.src}
|
|
||||||
srcSet={data.srcSet}
|
|
||||||
alt={data.name}
|
|
||||||
height={data.height ? `${data.height}px` : null}
|
|
||||||
width={data.width ? `${data.width}px` : null}
|
|
||||||
/>
|
|
||||||
</figure>
|
|
||||||
{data.favorite && (<figure class="ffz--favorite ffz-i-star" />)}
|
|
||||||
{locked && (<figure class="ffz-i-lock" />)}
|
|
||||||
</button>)
|
</button>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (<div class="tw-absolute tw-balloon tw-balloon--up tw-balloon--right tw-balloon tw-block">
|
||||||
|
<div class="tw-border-b tw-border-l tw-border-r tw-border-t tw-border-radius-medium tw-c-background tw-elevation-1">
|
||||||
|
{this.renderTone(emoji, null)}
|
||||||
|
{tones}
|
||||||
|
</div>
|
||||||
|
</div>);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (<figure
|
||||||
|
class="ffz--emoji-tone-picker__emoji"
|
||||||
|
style={{
|
||||||
|
backgroundPosition: `${x_pct}% ${y_pct}%`
|
||||||
|
}}
|
||||||
|
/>)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const emoji = this.state.emoji,
|
||||||
|
tone = this.props.tone,
|
||||||
|
toned = tone && emoji.variants[tone];
|
||||||
|
|
||||||
|
return (<div ref={this.saveRef} class="ffz--emoji-tone-picker tw-relative tw-mg-l-1">
|
||||||
|
<button
|
||||||
|
class="tw-interactive tw-button tw-button--dropmenu tw-button--hollow"
|
||||||
|
onClick={this.onClick}
|
||||||
|
onMouseEnter={this.onMouseEnter}
|
||||||
|
>
|
||||||
|
<span class="tw-button__text">
|
||||||
|
{this.renderEmoji(toned || emoji)}
|
||||||
|
</span>
|
||||||
|
<span class="tw-button__icon tw-button__icon--right">
|
||||||
|
<figure class="ffz-i-down-dir" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
{this.renderToneMenu()}
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.MenuSection = class FFZMenuSection extends React.Component {
|
this.MenuSection = class FFZMenuSection extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -340,10 +444,19 @@ export default class EmoteMenu extends Module {
|
||||||
this.state = {collapsed: props.data && collapsed.includes(props.data.key)}
|
this.state = {collapsed: props.data && collapsed.includes(props.data.key)}
|
||||||
|
|
||||||
this.clickHeading = this.clickHeading.bind(this);
|
this.clickHeading = this.clickHeading.bind(this);
|
||||||
|
this.clickEmote = this.clickEmote.bind(this);
|
||||||
|
|
||||||
this.onMouseEnter = this.onMouseEnter.bind(this);
|
this.onMouseEnter = this.onMouseEnter.bind(this);
|
||||||
this.onMouseLeave = this.onMouseLeave.bind(this);
|
this.onMouseLeave = this.onMouseLeave.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clickEmote(event) {
|
||||||
|
if ( t.emotes.handleClick(event) )
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.props.onClickEmote(event.currentTarget.dataset.name)
|
||||||
|
}
|
||||||
|
|
||||||
clickHeading() {
|
clickHeading() {
|
||||||
if ( this.props.filtered )
|
if ( this.props.filtered )
|
||||||
return;
|
return;
|
||||||
|
@ -444,15 +557,25 @@ export default class EmoteMenu extends Module {
|
||||||
const data = this.props.data,
|
const data = this.props.data,
|
||||||
filtered = this.props.filtered,
|
filtered = this.props.filtered,
|
||||||
lock = data.locks && data.locks[this.state.unlocked],
|
lock = data.locks && data.locks[this.state.unlocked],
|
||||||
emotes = data.filtered_emotes && data.filtered_emotes.map(emote => (! filtered || ! emote.locked) && (<t.MenuEmote
|
|
||||||
key={emote.id}
|
emotes = data.filtered_emotes && data.filtered_emotes.map(emote => {
|
||||||
onClickEmote={this.props.onClickEmote}
|
if ( filtered && emote.locked )
|
||||||
data={emote}
|
return;
|
||||||
source={show_sources}
|
|
||||||
locked={emote.locked && (! lock || ! lock.emotes.has(emote.id))}
|
const locked = emote.locked && (! lock || ! lock.emotes.has(emote.id)),
|
||||||
all_locked={data.all_locked}
|
emote_lock = locked && data.locks && data.locks[emote.set_id],
|
||||||
lock={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 (<div class="tw-pd-1 tw-border-b tw-c-background-alt tw-align-center">
|
return (<div class="tw-pd-1 tw-border-b tw-c-background-alt tw-align-center">
|
||||||
{emotes}
|
{emotes}
|
||||||
|
@ -460,6 +583,38 @@ export default class EmoteMenu extends Module {
|
||||||
</div>)
|
</div>)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderEmote(emote, locked, source, sellout) {
|
||||||
|
return (<button
|
||||||
|
key={emote.id}
|
||||||
|
class={`ffz-tooltip emote-picker__emote-link${locked ? ' locked' : ''}`}
|
||||||
|
data-tooltip-type="emote"
|
||||||
|
data-provider={emote.provider}
|
||||||
|
data-id={emote.id}
|
||||||
|
data-set={emote.set_id}
|
||||||
|
data-code={emote.code}
|
||||||
|
data-variant={emote.variant}
|
||||||
|
data-no-source={source}
|
||||||
|
data-name={emote.name}
|
||||||
|
aria-label={emote.name}
|
||||||
|
data-locked={emote.locked}
|
||||||
|
data-sellout={sellout}
|
||||||
|
onClick={!emote.locked && this.clickEmote}
|
||||||
|
>
|
||||||
|
<figure class="emote-picker__emote-figure">
|
||||||
|
<img
|
||||||
|
class={`emote-picker__emote-image${emote.emoji ? ' ffz-emoji' : ''}`}
|
||||||
|
src={emote.src}
|
||||||
|
srcSet={emote.srcSet}
|
||||||
|
alt={emote.name}
|
||||||
|
height={emote.height ? `${emote.height}px` : null}
|
||||||
|
width={emote.width ? `${emote.width}px` : null}
|
||||||
|
/>
|
||||||
|
</figure>
|
||||||
|
{emote.favorite && <figure class="ffz--favorite ffz-i-star" />}
|
||||||
|
{locked && <figure class="ffz-i-lock" />}
|
||||||
|
</button>)
|
||||||
|
}
|
||||||
|
|
||||||
renderSellout() {
|
renderSellout() {
|
||||||
const data = this.props.data;
|
const data = this.props.data;
|
||||||
|
|
||||||
|
@ -475,7 +630,7 @@ export default class EmoteMenu extends Module {
|
||||||
<div class="ffz--sub-buttons tw-mg-t-05">
|
<div class="ffz--sub-buttons tw-mg-t-05">
|
||||||
{Object.values(data.locks).map(lock => (<a
|
{Object.values(data.locks).map(lock => (<a
|
||||||
key={lock.price}
|
key={lock.price}
|
||||||
class="tw-button"
|
class="tw-button tw-border-radius-none"
|
||||||
href={lock.url}
|
href={lock.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
|
@ -494,14 +649,55 @@ export default class EmoteMenu extends Module {
|
||||||
|
|
||||||
this.fine.wrap('ffz-menu-section', this.MenuSection);
|
this.fine.wrap('ffz-menu-section', this.MenuSection);
|
||||||
|
|
||||||
|
this.EmojiSection = class FFZMenuEmojiSection extends this.MenuSection {
|
||||||
|
renderEmote(emote, locked, source, sellout) {
|
||||||
|
const emoji_x = (emote.x * (t.emoji_size + 2)) + 1,
|
||||||
|
emoji_y = (emote.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 (<button
|
||||||
|
key={emote.id}
|
||||||
|
class={`ffz-tooltip emote-picker__emote-link${locked ? ' locked' : ''}${emote.emoji ? ' emote-picker__emoji' : ''}`}
|
||||||
|
data-tooltip-type="emote"
|
||||||
|
data-provider={emote.provider}
|
||||||
|
data-id={emote.id}
|
||||||
|
data-set={emote.set_id}
|
||||||
|
data-code={emote.code}
|
||||||
|
data-variant={emote.variant}
|
||||||
|
data-no-source={source}
|
||||||
|
data-name={emote.name}
|
||||||
|
aria-label={emote.name}
|
||||||
|
data-locked={emote.locked}
|
||||||
|
data-sellout={sellout}
|
||||||
|
onClick={!emote.locked && this.clickEmote}
|
||||||
|
>
|
||||||
|
<figure
|
||||||
|
class="emote-picker__emote-figure"
|
||||||
|
style={{
|
||||||
|
backgroundPosition: `${x_pct}% ${y_pct}%`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{emote.favorite && <figure class="ffz--favorite ffz-i-star" />}
|
||||||
|
{locked && <figure class="ffz-i-lock" />}
|
||||||
|
</button>)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
this.MenuComponent = class FFZEmoteMenuComponent extends React.Component {
|
this.MenuComponent = class FFZEmoteMenuComponent extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {tab: null}
|
this.state = {
|
||||||
|
tab: null,
|
||||||
|
tone: t.settings.provider.get('emoji-tone', null)
|
||||||
|
}
|
||||||
|
|
||||||
this.componentWillReceiveProps(props);
|
this.componentWillReceiveProps(props);
|
||||||
|
|
||||||
|
this.pickTone = this.pickTone.bind(this);
|
||||||
this.clickTab = this.clickTab.bind(this);
|
this.clickTab = this.clickTab.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);
|
||||||
|
@ -517,6 +713,17 @@ export default class EmoteMenu extends Module {
|
||||||
window.ffz_menu = null;
|
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) {
|
clickTab(event) {
|
||||||
this.setState({
|
this.setState({
|
||||||
tab: event.target.dataset.tab
|
tab: event.target.dataset.tab
|
||||||
|
@ -583,27 +790,24 @@ export default class EmoteMenu extends Module {
|
||||||
|
|
||||||
this.setState({loading: true}, () => {
|
this.setState({loading: true}, () => {
|
||||||
t.getData(sets, force).then(d => {
|
t.getData(sets, force).then(d => {
|
||||||
|
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.setState(this.filterState(this.state.filter, this.buildState(
|
||||||
this.props,
|
this.props,
|
||||||
Object.assign({}, this.state, {set_sets: sets, set_data: d, loading: false})
|
Object.assign({}, this.state, {set_sets: sets, set_data: d, loading: false})
|
||||||
)));
|
)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return true;
|
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) {
|
filterState(input, old_state) {
|
||||||
const state = Object.assign({}, 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
|
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 = [],
|
sets = state.emoji_sets = [],
|
||||||
emoji_favorites = t.emotes.getFavorites('emoji'),
|
emoji_favorites = t.emotes.getFavorites('emoji'),
|
||||||
style = t.chat.context.get('chat.emoji.style') || 'twitter',
|
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 = {};
|
categories = {};
|
||||||
|
|
||||||
for(const emoji of Object.values(t.emoji.emoji)) {
|
for(const emoji of Object.values(t.emoji.emoji)) {
|
||||||
if ( ! emoji.has[style] )
|
if ( ! emoji.has[style] || emoji.category === 'Skin Tones' )
|
||||||
continue;
|
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];
|
let cat = categories[emoji.category];
|
||||||
if ( ! cat ) {
|
if ( ! cat ) {
|
||||||
cat = categories[emoji.category] = [];
|
cat = categories[emoji.category] = [];
|
||||||
|
|
||||||
sets.push({
|
sets.push({
|
||||||
key: `emoji-${emoji.category}`,
|
key: `emoji-${emoji.category}`,
|
||||||
|
emoji: true,
|
||||||
image: t.emoji.getFullImage(emoji.image),
|
image: t.emoji.getFullImage(emoji.image),
|
||||||
title: emoji.category,
|
title: emoji.category,
|
||||||
source: t.i18n.t('emote-menu.emoji', 'Emoji'),
|
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),
|
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 = {
|
em = {
|
||||||
provider: 'emoji',
|
provider: 'emoji',
|
||||||
emoji: true,
|
emoji: true,
|
||||||
code: emoji.code,
|
code: emoji.code,
|
||||||
name: emoji.raw,
|
name: source.raw,
|
||||||
|
variant: has_tone && tone,
|
||||||
|
|
||||||
search: emoji.names[0],
|
search: emoji.names[0],
|
||||||
|
|
||||||
height: 18,
|
height: 18,
|
||||||
width: 18,
|
width: 18,
|
||||||
|
|
||||||
x: emoji.sheet_x,
|
x: source.sheet_x,
|
||||||
y: emoji.sheet_y,
|
y: source.sheet_y,
|
||||||
|
|
||||||
favorite: is_fav,
|
favorite: is_fav,
|
||||||
|
|
||||||
src: t.emoji.getFullImage(emoji.image),
|
src: t.emoji.getFullImage(source.image),
|
||||||
srcSet: t.emoji.getFullImageSet(emoji.image)
|
srcSet: t.emoji.getFullImageSet(source.image)
|
||||||
};
|
};
|
||||||
|
|
||||||
cat.push(em);
|
cat.push(em);
|
||||||
|
@ -713,10 +932,23 @@ export default class EmoteMenu extends Module {
|
||||||
|
|
||||||
state.has_emoji_tab = sets.length > 0;
|
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) {
|
buildState(props, old_state) {
|
||||||
const state = Object.assign({}, old_state),
|
const state = Object.assign({}, old_state),
|
||||||
|
|
||||||
|
@ -735,7 +967,7 @@ export default class EmoteMenu extends Module {
|
||||||
return state;
|
return state;
|
||||||
|
|
||||||
// Sorters
|
// 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_tiers = t.chat.context.get('chat.emote-menu.sort-tiers-last'),
|
||||||
sort_emotes = (a,b) => {
|
sort_emotes = (a,b) => {
|
||||||
if ( a.inventory || b.inventory )
|
if ( a.inventory || b.inventory )
|
||||||
|
@ -752,6 +984,7 @@ export default class EmoteMenu extends Module {
|
||||||
return sorter(a,b);
|
return sorter(a,b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Start with the All tab. Some data calculated for
|
// Start with the All tab. Some data calculated for
|
||||||
// all is re-used for the Channel tab.
|
// 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),
|
const set_id = parseInt(emote_set.id, 10),
|
||||||
is_inventory = inventory.has(set_id),
|
is_inventory = inventory.has(set_id),
|
||||||
set_data = data[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_data.image,
|
||||||
image_set = set_data.image_set;
|
image_set = set_data.image_set;
|
||||||
|
|
||||||
|
@ -802,6 +1035,11 @@ export default class EmoteMenu extends Module {
|
||||||
icon = 'inventory';
|
icon = 'inventory';
|
||||||
sort_key = 50;
|
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 ) {
|
} else if ( more_data ) {
|
||||||
title = more_data.c_name;
|
title = more_data.c_name;
|
||||||
|
|
||||||
|
@ -1028,15 +1266,6 @@ export default class EmoteMenu extends Module {
|
||||||
|
|
||||||
state.has_channel_tab = channel.length > 0;
|
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);
|
return this.buildEmoji(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1125,8 +1354,6 @@ export default class EmoteMenu extends Module {
|
||||||
if ( props.visible )
|
if ( props.visible )
|
||||||
this.loadData();
|
this.loadData();
|
||||||
|
|
||||||
this.loadSets(props);
|
|
||||||
|
|
||||||
const state = this.buildState(props, this.state);
|
const state = this.buildState(props, this.state);
|
||||||
this.setState(this.filterState(state.filter, 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) )
|
if ( (tab === 'channel' && ! this.state.has_channel_tab) || (tab === 'emoji' && ! this.state.has_emoji_tab) )
|
||||||
tab = 'all';
|
tab = 'all';
|
||||||
|
|
||||||
|
const is_emoji = tab === 'emoji';
|
||||||
|
|
||||||
switch(tab) {
|
switch(tab) {
|
||||||
case 'fav':
|
case 'fav':
|
||||||
sets = this.state.filtered_fav_sets;
|
sets = this.state.filtered_fav_sets;
|
||||||
|
@ -1213,30 +1442,42 @@ export default class EmoteMenu extends Module {
|
||||||
<div class="simplebar-scroll-content">
|
<div class="simplebar-scroll-content">
|
||||||
<div class="simplebar-content">
|
<div class="simplebar-content">
|
||||||
{loading && this.renderLoading()}
|
{loading && this.renderLoading()}
|
||||||
{!loading && sets && sets.map(data => (<t.MenuSection
|
{!loading && sets && sets.map(data => createElement(
|
||||||
key={data.key}
|
data.emoji ? t.EmojiSection : t.MenuSection,
|
||||||
data={data}
|
{
|
||||||
filtered={this.state.filtered}
|
key: data.key,
|
||||||
onClickEmote={this.props.onClickEmote}
|
data,
|
||||||
/>))}
|
filtered: this.state.filtered,
|
||||||
|
onClickEmote: this.props.onClickEmote
|
||||||
|
}
|
||||||
|
))}
|
||||||
{! loading && (! sets || ! sets.length) && this.renderEmpty()}
|
{! loading && (! sets || ! sets.length) && this.renderEmpty()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="emote-picker__controls-container tw-relative">
|
<div class="emote-picker__controls-container tw-relative">
|
||||||
{t.chat.context.get('chat.emote-menu.show-search') && (<div class="tw-border-t tw-pd-1">
|
{(is_emoji || t.chat.context.get('chat.emote-menu.show-search')) && (<div class="tw-border-t tw-pd-1">
|
||||||
<div class="tw-relative">
|
<div class="tw-flex">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05"
|
class="tw-block tw-border-radius-medium tw-font-size-6 tw-full-width tw-input tw-pd-x-1 tw-pd-y-05"
|
||||||
onChange={this.handleFilterChange}
|
onChange={this.handleFilterChange}
|
||||||
onKeyDown={this.handleKeyDown}
|
onKeyDown={this.handleKeyDown}
|
||||||
placeholder={t.i18n.t('emote-menu.search', 'Search for Emotes')}
|
placeholder={
|
||||||
|
is_emoji ?
|
||||||
|
t.i18n.t('emote-menu.search-emoji', 'Search for Emoji') :
|
||||||
|
t.i18n.t('emote-menu.search', 'Search for Emotes')
|
||||||
|
}
|
||||||
value={this.state.filter}
|
value={this.state.filter}
|
||||||
autoFocus
|
autoFocus
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
/>
|
/>
|
||||||
|
{is_emoji && <t.EmojiTonePicker
|
||||||
|
tone={this.state.tone}
|
||||||
|
choices={this.state.tone_emoji}
|
||||||
|
pickTone={this.pickTone}
|
||||||
|
/>}
|
||||||
</div>
|
</div>
|
||||||
</div>)}
|
</div>)}
|
||||||
<div class="emote-picker__tabs-container tw-flex tw-border-t tw-c-background">
|
<div class="emote-picker__tabs-container tw-flex tw-border-t tw-c-background">
|
||||||
|
|
|
@ -147,6 +147,7 @@ export default class TabCompletion extends Module {
|
||||||
getEmojiSuggestions(input, inst) {
|
getEmojiSuggestions(input, inst) {
|
||||||
let search = input.slice(1).toLowerCase();
|
let search = input.slice(1).toLowerCase();
|
||||||
const style = this.chat.context.get('chat.emoji.style'),
|
const style = this.chat.context.get('chat.emoji.style'),
|
||||||
|
tone = this.settings.provider.get('emoji-tone', null),
|
||||||
results = [],
|
results = [],
|
||||||
has_colon = search.endsWith(':');
|
has_colon = search.endsWith(':');
|
||||||
|
|
||||||
|
@ -155,16 +156,19 @@ export default class TabCompletion extends Module {
|
||||||
|
|
||||||
for(const name in this.emoji.names)
|
for(const name in this.emoji.names)
|
||||||
if ( has_colon ? name === search : name.startsWith(search) ) {
|
if ( has_colon ? name === search : name.startsWith(search) ) {
|
||||||
const emoji = this.emoji.emoji[this.emoji.names[name]];
|
const emoji = this.emoji.emoji[this.emoji.names[name]],
|
||||||
if ( emoji && (style === 0 || emoji.has[style]) )
|
toned = emoji.variants && emoji.variants[tone],
|
||||||
|
source = toned || emoji;
|
||||||
|
|
||||||
|
if ( emoji && (style === 0 || source.has[style]) )
|
||||||
results.push({
|
results.push({
|
||||||
current: input,
|
current: input,
|
||||||
replacement: emoji.raw,
|
replacement: source.raw,
|
||||||
element: inst.renderFFZEmojiSuggestion({
|
element: inst.renderFFZEmojiSuggestion({
|
||||||
token: `:${name}:`,
|
token: `:${name}:`,
|
||||||
id: `emoji-${emoji.code}`,
|
id: `emoji-${emoji.code}`,
|
||||||
src: this.emoji.getFullImage(emoji.image, style),
|
src: this.emoji.getFullImage(source.image, style),
|
||||||
srcSet: this.emoji.getFullImageSet(emoji.image, style)
|
srcSet: this.emoji.getFullImageSet(source.image, style)
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default class Player extends Module {
|
||||||
|
|
||||||
this.PersistentPlayer = this.fine.define(
|
this.PersistentPlayer = this.fine.define(
|
||||||
'twitch-player-persistent',
|
'twitch-player-persistent',
|
||||||
n => n.renderMiniControl && n.renderMiniTitle && n.handleWindowResize,
|
n => n.renderMiniHoverControls && n.togglePause,
|
||||||
['front-page', 'user', 'video', 'dash']
|
['front-page', 'user', 'video', 'dash']
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -32,13 +32,16 @@ export default class SubButton extends Module {
|
||||||
|
|
||||||
this.SubButton = this.fine.define(
|
this.SubButton = this.fine.define(
|
||||||
'sub-button',
|
'sub-button',
|
||||||
n => n.reportSubMenuAction && n.isUserDataReady,
|
n => n.handleSubMenuAction && n.isUserDataReady,
|
||||||
['user', 'video']
|
['user', 'video']
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnable() {
|
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('mount', this.updateSubButton, this);
|
||||||
this.SubButton.on('update', this.updateSubButton, this);
|
this.SubButton.on('update', this.updateSubButton, this);
|
||||||
|
@ -47,7 +50,7 @@ export default class SubButton extends Module {
|
||||||
|
|
||||||
updateSubButton(inst) {
|
updateSubButton(inst) {
|
||||||
const container = this.fine.getChildNode(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 )
|
if ( ! btn )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -62,7 +65,6 @@ export default class SubButton extends Module {
|
||||||
btn.insertBefore(<span class="tw-button__icon tw-button__icon--left ffz--can-prime">
|
btn.insertBefore(<span class="tw-button__icon tw-button__icon--left ffz--can-prime">
|
||||||
<figure
|
<figure
|
||||||
class="ffz-i-crown ffz-tooltip"
|
class="ffz-i-crown ffz-tooltip"
|
||||||
data-tooltip-type="html"
|
|
||||||
data-title={this.i18n.t('sub-button.prime', 'Your free channel sub with Prime is available.')}
|
data-title={this.i18n.t('sub-button.prime', 'Your free channel sub with Prime is available.')}
|
||||||
/>
|
/>
|
||||||
</span>, btn.firstElementChild);
|
</span>, btn.firstElementChild);
|
||||||
|
|
|
@ -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 {
|
.emote-picker__emote-link {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
@ -187,6 +203,15 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.emote-picker__emoji {
|
||||||
|
min-width: unset;
|
||||||
|
|
||||||
|
.emote-picker__emote-figure {
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.locked {
|
&.locked {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
|
|
@ -57,9 +57,9 @@
|
||||||
|
|
||||||
|
|
||||||
.tw-button__icon .ffz-i-crown:before {
|
.tw-button__icon .ffz-i-crown:before {
|
||||||
margin: 0;
|
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
vertical-align: sub;
|
vertical-align: middle;
|
||||||
|
margin-bottom: -.6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ffz-i-cancel:before { content: '\e800'; } /* '' */
|
.ffz-i-cancel:before { content: '\e800'; } /* '' */
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue