mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-02 16:08:31 +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;
|
||||
|
||||
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: () =>
|
||||
|
|
|
@ -46,9 +46,11 @@ export const open_url = {
|
|||
const url = process(data.options.url, data, this.i18n.locale);
|
||||
|
||||
const win = window.open();
|
||||
if ( win ) {
|
||||
win.opener = null;
|
||||
win.location = url;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -225,9 +225,11 @@ export default class Emotes extends Module {
|
|||
|
||||
if ( url ) {
|
||||
const win = window.open();
|
||||
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);
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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,48 +330,111 @@ 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});
|
||||
|
||||
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
|
||||
class={`ffz-tooltip emote-picker__emote-link${locked ? ' locked' : ''}`}
|
||||
data-tooltip-type="emote"
|
||||
data-provider={data.provider}
|
||||
data-id={data.id}
|
||||
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}
|
||||
key={data.code}
|
||||
data-tone={tone}
|
||||
class="tw-interactive tw-block tw-full-width tw-interactable tw-interactable--inverted tw-pd-y-05 tw-pd-x-2"
|
||||
onClick={this.clickTone}
|
||||
>
|
||||
<figure class="emote-picker__emote-figure">
|
||||
<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" />)}
|
||||
{this.renderEmoji(data)}
|
||||
</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 {
|
||||
constructor(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) && (<t.MenuEmote
|
||||
key={emote.id}
|
||||
onClickEmote={this.props.onClickEmote}
|
||||
data={emote}
|
||||
source={show_sources}
|
||||
locked={emote.locked && (! lock || ! lock.emotes.has(emote.id))}
|
||||
all_locked={data.all_locked}
|
||||
lock={data.locks && data.locks[emote.set_id]}
|
||||
/>));
|
||||
|
||||
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 (<div class="tw-pd-1 tw-border-b tw-c-background-alt tw-align-center">
|
||||
{emotes}
|
||||
|
@ -460,6 +583,38 @@ export default class EmoteMenu extends Module {
|
|||
</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() {
|
||||
const data = this.props.data;
|
||||
|
||||
|
@ -475,7 +630,7 @@ export default class EmoteMenu extends Module {
|
|||
<div class="ffz--sub-buttons tw-mg-t-05">
|
||||
{Object.values(data.locks).map(lock => (<a
|
||||
key={lock.price}
|
||||
class="tw-button"
|
||||
class="tw-button tw-border-radius-none"
|
||||
href={lock.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
|
@ -494,14 +649,55 @@ export default class EmoteMenu extends Module {
|
|||
|
||||
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 {
|
||||
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 => {
|
||||
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 {
|
|||
<div class="simplebar-scroll-content">
|
||||
<div class="simplebar-content">
|
||||
{loading && this.renderLoading()}
|
||||
{!loading && sets && sets.map(data => (<t.MenuSection
|
||||
key={data.key}
|
||||
data={data}
|
||||
filtered={this.state.filtered}
|
||||
onClickEmote={this.props.onClickEmote}
|
||||
/>))}
|
||||
{!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()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<div class="tw-relative">
|
||||
{(is_emoji || t.chat.context.get('chat.emote-menu.show-search')) && (<div class="tw-border-t tw-pd-1">
|
||||
<div class="tw-flex">
|
||||
<input
|
||||
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"
|
||||
onChange={this.handleFilterChange}
|
||||
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}
|
||||
autoFocus
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
/>
|
||||
{is_emoji && <t.EmojiTonePicker
|
||||
tone={this.state.tone}
|
||||
choices={this.state.tone_emoji}
|
||||
pickTone={this.pickTone}
|
||||
/>}
|
||||
</div>
|
||||
</div>)}
|
||||
<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) {
|
||||
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)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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']
|
||||
);
|
||||
|
||||
|
|
|
@ -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(<span class="tw-button__icon tw-button__icon--left ffz--can-prime">
|
||||
<figure
|
||||
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.')}
|
||||
/>
|
||||
</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 {
|
||||
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;
|
||||
|
||||
|
|
|
@ -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'; } /* '' */
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue