1
0
Fork 0
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:
SirStendec 2018-07-26 19:40:53 -04:00
parent 2869eaedd8
commit 43832890b8
10 changed files with 434 additions and 120 deletions

View file

@ -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: () =>

View file

@ -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;
}
}
};

View file

@ -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);

View file

@ -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/

View file

@ -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">

View file

@ -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)
})
});
}

View file

@ -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']
);

View file

@ -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);

View file

@ -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;

View file

@ -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'; } /* '' */