723 lines
22 KiB
JavaScript
Executable file
723 lines
22 KiB
JavaScript
Executable file
import React from 'react';
|
|
import PureRenderMixin from 'react-addons-pure-render-mixin';
|
|
import LazyScroller, {Themes} from './common/LazyScroller';
|
|
import PopoutActionCreators from '../actions/PopoutActionCreators';
|
|
import UserSettingsModalActionCreators from '../actions/UserSettingsModalActionCreators';
|
|
import Popout from './common/Popout';
|
|
import lodash from 'lodash';
|
|
import classNames from 'classnames';
|
|
import i18n from '../i18n';
|
|
import SearchBar from './common/SearchBar';
|
|
import NoSearchResults from './common/NoSearchResults';
|
|
import PopoutStore from '../stores/PopoutStore';
|
|
import Flux from '../lib/flux';
|
|
import twemoji from 'twemoji';
|
|
import EmojiStore from '../stores/EmojiStore';
|
|
import GuildStore from '../stores/GuildStore';
|
|
import SelectedGuildStore from '../stores/SelectedGuildStore';
|
|
import SortedGuildStore from '../stores/SortedGuildStore';
|
|
import SelectedChannelStore from '../stores/SelectedChannelStore';
|
|
import ChannelStore from '../stores/ChannelStore';
|
|
import EmojiActionCreators from '../actions/EmojiActionCreators';
|
|
import Animated from '../lib/animated';
|
|
import UnicodeEmojis from '../lib/UnicodeEmojis';
|
|
import ReactDOM from 'react-dom';
|
|
import AnalyticsUtils from '../utils/AnalyticsUtils';
|
|
import EmojiUtils from '../utils/EmojiUtils';
|
|
import {
|
|
EmojiSprites,
|
|
ComponentActions,
|
|
DIVERSITY_SURROGATES,
|
|
UserSettingsSections,
|
|
EMOJI_CATEGORY_CUSTOM,
|
|
EMOJI_CATEGORY_RECENT,
|
|
} from '../Constants';
|
|
import '../styles/emoji_picker.styl';
|
|
|
|
const ELEMENT_HEIGHT = 32;
|
|
|
|
const EMOJI_OK_HAND = twemoji.convert.fromCodePoint('1f44c'); // ok_hand
|
|
const EMOJI_SPRITE_SIZE = 22;
|
|
const ICON_FADE_IN = 150;
|
|
const ICON_DELAY_OFFSET = 20;
|
|
const SELECTOR_EXPAND_DURATION = 125;
|
|
|
|
const DiversityIcon = React.createClass({
|
|
getDefaultProps() {
|
|
return {
|
|
delay: 0,
|
|
fade: false,
|
|
};
|
|
},
|
|
|
|
getInitialState() {
|
|
return {
|
|
opacity: new Animated.Value(this.props.fade ? 0 : 1),
|
|
};
|
|
},
|
|
|
|
componentDidMount() {
|
|
if (this.props.fade) {
|
|
Animated.timing(this.state.opacity, {delay: this.props.delay, toValue: 1, duration: ICON_FADE_IN}).start();
|
|
}
|
|
},
|
|
|
|
render() {
|
|
const url = EmojiUtils.getURL(EMOJI_OK_HAND + this.props.surrogate);
|
|
const style = {
|
|
opacity: this.state.opacity,
|
|
backgroundImage: `url("${url}")`,
|
|
};
|
|
|
|
return <Animated.div onClick={() => this.props.onClick(this.props.surrogate)} className="item" style={style} />;
|
|
},
|
|
});
|
|
|
|
const CLOSED_HEIGHT = 28;
|
|
const DiversitySelector = React.createClass({
|
|
mixins: [PureRenderMixin, Flux.StoreListenerMixin(EmojiStore)],
|
|
|
|
getInitialState() {
|
|
return {
|
|
isOpen: false,
|
|
height: new Animated.Value(CLOSED_HEIGHT),
|
|
};
|
|
},
|
|
|
|
getStateFromStores() {
|
|
return {
|
|
diversitySurrogate: EmojiStore.diversitySurrogate,
|
|
};
|
|
},
|
|
|
|
handleDiversityColorChange(selectedColor) {
|
|
EmojiActionCreators.setDiversityColor(selectedColor);
|
|
this.close(null);
|
|
},
|
|
|
|
handleOpen() {
|
|
Animated.timing(this.state.height, {
|
|
fromValue: CLOSED_HEIGHT,
|
|
toValue: 30 * 6,
|
|
duration: SELECTOR_EXPAND_DURATION,
|
|
}).start();
|
|
this.setState({isOpen: true});
|
|
document.addEventListener('click', this.close, true);
|
|
this.props.onOpen && this.props.onOpen(true);
|
|
},
|
|
|
|
componentWillUnmount() {
|
|
document.removeEventListener('click', this.close, true);
|
|
},
|
|
|
|
close(e) {
|
|
if (e != null) {
|
|
let target = e.target;
|
|
const me = ReactDOM.findDOMNode(this);
|
|
while (target != null) {
|
|
if (target === me) return;
|
|
target = target.parentNode;
|
|
}
|
|
}
|
|
this.setState({isOpen: false});
|
|
this.state.height.setValue(CLOSED_HEIGHT);
|
|
this.props.onOpen && this.props.onOpen(false);
|
|
},
|
|
|
|
render() {
|
|
const selectedSurrogate = this.state.diversitySurrogate;
|
|
if (this.state.isOpen) {
|
|
const surrogates = ['', ...DIVERSITY_SURROGATES];
|
|
lodash.remove(surrogates, s => s === selectedSurrogate);
|
|
surrogates.unshift(selectedSurrogate);
|
|
const icons = surrogates.map((s, index) =>
|
|
<DiversityIcon
|
|
fade={index > 0}
|
|
delay={index * ICON_DELAY_OFFSET}
|
|
key={index}
|
|
surrogate={s}
|
|
onClick={this.handleDiversityColorChange}
|
|
/>
|
|
);
|
|
|
|
return (
|
|
<div className="diversity-selector">
|
|
<Animated.div className="popout" style={{height: this.state.height}}>
|
|
{icons}
|
|
</Animated.div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const url = EmojiUtils.getURL(EMOJI_OK_HAND + selectedSurrogate);
|
|
return (
|
|
<div className="diversity-selector">
|
|
<div onClick={this.handleOpen} className="item" style={{backgroundImage: `url("${url}")`}} />
|
|
</div>
|
|
);
|
|
},
|
|
});
|
|
|
|
export const EmojiPicker = React.createClass({
|
|
mixins: [PureRenderMixin, Flux.StoreListenerMixin(EmojiStore, SelectedGuildStore, SelectedChannelStore)],
|
|
|
|
getStateFromStores() {
|
|
return {
|
|
diversitySurrogate: EmojiStore.diversitySurrogate,
|
|
selectedChannel: ChannelStore.getChannel(SelectedChannelStore.getChannelId()),
|
|
selectedGuildId: SelectedGuildStore.getGuildId(),
|
|
};
|
|
},
|
|
|
|
getInitialState() {
|
|
return {
|
|
premiumPromoOpen: false,
|
|
diversityPickerOpen: false,
|
|
metaData: this.computeMetaData(null),
|
|
searchResults: null,
|
|
currentSection: EmojiStore.categories[0],
|
|
selectedRow: -1,
|
|
selectedColumn: -1,
|
|
query: '',
|
|
placeholder: i18n.Messages.SEARCH_FOR_EMOJI,
|
|
};
|
|
},
|
|
|
|
selectEmoji(emojiObject, close = true) {
|
|
if (emojiObject != null && EmojiUtils.isEmojiDisabled(emojiObject, this.state.selectedChannel)) {
|
|
this.setState({premiumPromoOpen: true});
|
|
return;
|
|
}
|
|
|
|
if (this.props.onSelectEmoji) {
|
|
this.props.onSelectEmoji(emojiObject);
|
|
}
|
|
|
|
if (close) {
|
|
PopoutActionCreators.close(this.props.popoutKey);
|
|
} else {
|
|
this.focusSearch();
|
|
}
|
|
},
|
|
|
|
clearNow(afterClear) {
|
|
const state = {
|
|
query: '',
|
|
placeholder: i18n.Messages.SEARCH_FOR_EMOJI,
|
|
searchResults: null,
|
|
metaData: this.computeMetaData(null),
|
|
selectedRow: -1,
|
|
selectedColumn: -1,
|
|
};
|
|
|
|
if (this.state.query !== '') {
|
|
state.currentSection = EmojiStore.categories[0];
|
|
}
|
|
|
|
this.focusSearch();
|
|
this.setState(state, afterClear);
|
|
},
|
|
|
|
handleClear() {
|
|
this.clearNow();
|
|
},
|
|
|
|
handleSelect(selectedRow, selectedColumn, e) {
|
|
if (selectedRow === null && selectedColumn === null) {
|
|
this.selectEmoji(null);
|
|
return;
|
|
}
|
|
|
|
this.selectEmoji(this.state.metaData[selectedRow].items[selectedColumn].emoji, !e.shiftKey);
|
|
},
|
|
|
|
handleSelectionChange(selectedRow, selectedColumn) {
|
|
if (selectedRow < 0 || selectedColumn < 0) {
|
|
return;
|
|
}
|
|
const {emoji, offsetTop} = this.state.metaData[selectedRow].items[selectedColumn];
|
|
this.setState({selectedRow, selectedColumn, placeholder: emoji.allNamesString});
|
|
this.refs.scroller.scrollIntoViewRect(offsetTop, offsetTop + ELEMENT_HEIGHT, false);
|
|
},
|
|
|
|
handleQueryChange(query) {
|
|
if (query === '') {
|
|
this.clearNow();
|
|
return;
|
|
}
|
|
|
|
// eat a leading : if it's present
|
|
let searchParam = query;
|
|
if (searchParam[0] === ':') {
|
|
searchParam = searchParam.slice(1);
|
|
}
|
|
|
|
const searchResults = EmojiStore.search(this.props.channel, searchParam);
|
|
|
|
this.setState({
|
|
query,
|
|
searchResults,
|
|
metaData: this.computeMetaData(searchResults),
|
|
selectedRow: -1,
|
|
selectedColumn: -1,
|
|
currentSection: null,
|
|
});
|
|
},
|
|
|
|
focusSearch() {
|
|
this.refs.search.focus();
|
|
},
|
|
|
|
componentWillMount() {
|
|
this.spriteSheetSize = [
|
|
`${EMOJI_SPRITE_SIZE * EmojiSprites.NonDiversityPerRow}px`,
|
|
`${EMOJI_SPRITE_SIZE * Math.ceil(UnicodeEmojis.numNonDiversitySprites / EmojiSprites.NonDiversityPerRow)}px`,
|
|
].join(' ');
|
|
|
|
this.diversitySpriteSheetSize = [
|
|
`${EMOJI_SPRITE_SIZE * EmojiSprites.DiversityPerRow}px`,
|
|
`${EMOJI_SPRITE_SIZE * Math.ceil(UnicodeEmojis.numDiversitySprites / EmojiSprites.DiversityPerRow)}px`,
|
|
].join(' ');
|
|
},
|
|
|
|
componentDidMount() {
|
|
AnalyticsUtils.track('Open Popout', {
|
|
Type: 'Emoji Picker',
|
|
});
|
|
|
|
this.focusSearch();
|
|
},
|
|
|
|
onHoverEmoji(emoji, selectedRow, selectedColumn) {
|
|
this.setState({selectedRow, selectedColumn, placeholder: emoji.allNamesString});
|
|
},
|
|
|
|
scrollNow(name) {
|
|
this.clearNow(() => {
|
|
this.refs.scroller.scrollTo(this.categoryOffsets[name], true);
|
|
});
|
|
},
|
|
|
|
getCurrentSection() {
|
|
if (this.state.query !== '') {
|
|
// if the user is searching, return the first category as selected but don't scroll around
|
|
return null;
|
|
}
|
|
|
|
const VISUAL_THRESHOLD = 0.25;
|
|
const scroller = this.refs.scroller.getScrollerNode();
|
|
const scrollOffset = scroller.scrollTop + scroller.offsetHeight * VISUAL_THRESHOLD;
|
|
|
|
if (scroller.scrollTop === 0) {
|
|
// first section no matter what
|
|
return EmojiStore.categories[0];
|
|
}
|
|
|
|
let currentSectionOffset = -1;
|
|
let currentSection = null;
|
|
lodash.forEach(this.categoryOffsets, (offsetTop, key) => {
|
|
if (offsetTop <= scrollOffset && offsetTop >= currentSectionOffset) {
|
|
currentSectionOffset = offsetTop;
|
|
currentSection = key;
|
|
}
|
|
});
|
|
|
|
return currentSection;
|
|
},
|
|
|
|
handleScroll() {
|
|
const currentSection = this.getCurrentSection();
|
|
|
|
if (currentSection != this.state.currentSection) {
|
|
this.setState({currentSection});
|
|
}
|
|
},
|
|
|
|
computeMetaData(optionalSearchResults) {
|
|
const emojiPerRow = EmojiStore.categories.length;
|
|
let selectedChannel = this.state && this.state.selectedChannel;
|
|
let selectedGuildId = this.state && this.state.selectedGuildId;
|
|
|
|
if (selectedChannel == null) {
|
|
selectedChannel = ChannelStore.getChannel(SelectedChannelStore.getChannelId());
|
|
}
|
|
|
|
if (selectedGuildId == null) {
|
|
selectedGuildId = SelectedGuildStore.getGuildId();
|
|
}
|
|
|
|
const metaData = [];
|
|
let visualRows = 0;
|
|
let row = 0;
|
|
|
|
const computeCategory = (categorizedEmoji, category = null) => {
|
|
let currentRowMetaData = [];
|
|
|
|
lodash.forEach(categorizedEmoji, emoji => {
|
|
if (EmojiUtils.isEmojiFiltered(emoji, selectedChannel)) {
|
|
return;
|
|
}
|
|
currentRowMetaData.push({
|
|
emoji,
|
|
offsetTop: visualRows * ELEMENT_HEIGHT,
|
|
row,
|
|
column: currentRowMetaData.length,
|
|
});
|
|
|
|
if (currentRowMetaData.length === emojiPerRow) {
|
|
metaData.push({category, items: currentRowMetaData});
|
|
currentRowMetaData = [];
|
|
row += 1;
|
|
visualRows += 1;
|
|
}
|
|
});
|
|
|
|
if (currentRowMetaData.length > 0) {
|
|
metaData.push({category, items: currentRowMetaData});
|
|
row += 1;
|
|
visualRows += 1;
|
|
}
|
|
};
|
|
|
|
if (optionalSearchResults) {
|
|
computeCategory(optionalSearchResults);
|
|
} else {
|
|
// if we are not searching, only do this once and then cache it for use later.
|
|
if (this.cachedMetaDataNoSearch) {
|
|
return this.cachedMetaDataNoSearch;
|
|
}
|
|
|
|
this.categoryOffsets = {};
|
|
this.categories = [];
|
|
|
|
const pushCategory = (category, title, emojis) => {
|
|
this.categoryOffsets[category] = visualRows * ELEMENT_HEIGHT;
|
|
this.categories.push({category, offsetTop: visualRows * ELEMENT_HEIGHT, title});
|
|
visualRows += 1;
|
|
computeCategory(emojis, category);
|
|
};
|
|
|
|
const emojiContext = EmojiStore.getDisambiguatedEmojiContext(selectedGuildId);
|
|
|
|
lodash.forEach(EmojiStore.categories, category => {
|
|
if (category === EMOJI_CATEGORY_CUSTOM) {
|
|
// Generate a separate category for each guild that has emoji.
|
|
let guildCategories = 0;
|
|
const customEmoji = emojiContext.getCustomEmoji();
|
|
const customEmojisByGuild = lodash.groupBy(customEmoji, 'guildId');
|
|
|
|
const pushGuildCategory = guild => {
|
|
const emojis = customEmojisByGuild[guild.id];
|
|
if (emojis == null || emojis.length === 0) return;
|
|
pushCategory(
|
|
// First category has to be just the custom category for scroll spy.
|
|
guildCategories++ === 0 ? category : `${category}${guildCategories}`,
|
|
guild.toString(),
|
|
emojis
|
|
);
|
|
};
|
|
const selectedGuild = GuildStore.getGuild(selectedGuildId);
|
|
if (selectedGuild) {
|
|
pushGuildCategory(selectedGuild);
|
|
}
|
|
|
|
lodash.each(SortedGuildStore.getSortedGuilds(), ({guild}) => {
|
|
if (guild.id !== selectedGuildId) {
|
|
pushGuildCategory(guild);
|
|
}
|
|
});
|
|
} else {
|
|
const emojis = category === EMOJI_CATEGORY_RECENT
|
|
? emojiContext.getFrequentlyUsedEmojis()
|
|
: UnicodeEmojis.getByCategory(category);
|
|
pushCategory(category, i18n.Messages[`EMOJI_CATEGORY_${category.toUpperCase()}`], emojis);
|
|
}
|
|
});
|
|
|
|
this.cachedMetaDataNoSearch = metaData;
|
|
}
|
|
|
|
return metaData;
|
|
},
|
|
|
|
handleDiversityPickerToggled(diversityPickerOpen) {
|
|
this.setState({diversityPickerOpen});
|
|
},
|
|
|
|
renderStickyHeader() {
|
|
if (this.state.query !== '') {
|
|
return null;
|
|
}
|
|
|
|
if (!this.refs.scroller) {
|
|
return (
|
|
<div className="sticky-header">
|
|
<div className="category">{this.categoryOffsets[EmojiStore.categories[0]]}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const scroller = this.refs.scroller.getScrollerNode();
|
|
const scrollOffset = scroller.scrollTop;
|
|
const headerBottom = scrollOffset + ELEMENT_HEIGHT;
|
|
|
|
let topSectionOffset = -1;
|
|
let topSectionIndex = null;
|
|
lodash.forEach(this.categories, ({offsetTop}, index) => {
|
|
if (offsetTop <= scrollOffset) {
|
|
if (topSectionOffset < offsetTop) {
|
|
topSectionOffset = offsetTop;
|
|
topSectionIndex = index;
|
|
}
|
|
}
|
|
});
|
|
|
|
const nextSectionIndex = topSectionIndex + 1;
|
|
|
|
if (nextSectionIndex < this.categories.length && this.categories[nextSectionIndex].offsetTop < headerBottom) {
|
|
const offsetStyle = {
|
|
marginTop: this.categories[nextSectionIndex].offsetTop - headerBottom,
|
|
};
|
|
return (
|
|
<div className="sticky-header" style={offsetStyle}>
|
|
<div className="category">{this.categories[topSectionIndex].title}</div>
|
|
<div className="category">{this.categories[nextSectionIndex].title}</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="sticky-header">
|
|
<div className="category">{this.categories[topSectionIndex].title}</div>
|
|
</div>
|
|
);
|
|
},
|
|
|
|
render() {
|
|
const rows = [];
|
|
let currentCategory = null;
|
|
let body = null;
|
|
|
|
if (this.state.metaData.length === 0) {
|
|
body = <NoSearchResults message={i18n.Messages.NO_EMOJI_SEARCH_RESULTS} />;
|
|
} else {
|
|
const surrogateCP = this.state.diversitySurrogate !== ''
|
|
? twemoji.convert.toCodePoint(this.state.diversitySurrogate)
|
|
: '';
|
|
|
|
lodash.forEach(this.state.metaData, ({category, items}) => {
|
|
if (category && category !== currentCategory) {
|
|
currentCategory = category;
|
|
const {title} = lodash.find(this.categories, c => c.category === category);
|
|
rows.push(<div key={category} name={category} className="category">{title}</div>);
|
|
}
|
|
|
|
const elements = lodash.map(items, ({emoji, row, column}) => {
|
|
let style = null;
|
|
const selected = row === this.state.selectedRow && column === this.state.selectedColumn;
|
|
const props = {
|
|
key: `${emoji.name}-${row}-${column}`,
|
|
className: classNames('emoji-item', {
|
|
selected,
|
|
disabled: EmojiUtils.isEmojiDisabled(emoji, this.state.selectedChannel),
|
|
}),
|
|
onClick: e => this.selectEmoji(emoji, !e.shiftKey),
|
|
onMouseOver: () => this.onHoverEmoji(emoji, row, column),
|
|
};
|
|
|
|
if (!emoji.useSpriteSheet) {
|
|
return <div {...props} style={{backgroundImage: `url('${emoji.url}')`}} />;
|
|
}
|
|
|
|
if (emoji.hasDiversity) {
|
|
const x = -emoji.index % EmojiSprites.DiversityPerRow * EMOJI_SPRITE_SIZE;
|
|
const y = -Math.floor(emoji.index / EmojiSprites.DiversityPerRow) * EMOJI_SPRITE_SIZE;
|
|
style = {
|
|
backgroundImage: `url('${require(`../images/emoji/spritesheet-${surrogateCP}.png`)}')`,
|
|
backgroundPosition: `${x}px ${y}px`,
|
|
backgroundSize: this.diversitySpriteSheetSize,
|
|
};
|
|
} else {
|
|
const x = -emoji.index % EmojiSprites.NonDiversityPerRow * EMOJI_SPRITE_SIZE;
|
|
const y = -Math.floor(emoji.index / EmojiSprites.NonDiversityPerRow) * EMOJI_SPRITE_SIZE;
|
|
style = {
|
|
backgroundImage: `url('${require(`../images/emoji/spritesheet-emoji.png`)}')`,
|
|
backgroundPosition: `${x}px ${y}px`,
|
|
backgroundSize: this.spriteSheetSize,
|
|
};
|
|
}
|
|
|
|
return (
|
|
<div {...props}>
|
|
<div className="sprite-item" style={style} />
|
|
</div>
|
|
);
|
|
});
|
|
rows.push(<div key={rows.length} className="row">{elements}</div>);
|
|
});
|
|
|
|
body = (
|
|
<LazyScroller
|
|
ref="scroller"
|
|
onScroll={this.handleScroll}
|
|
elementHeight={ELEMENT_HEIGHT}
|
|
fade
|
|
theme={Themes.LIGHT}
|
|
renderStickyHeader={() => this.renderStickyHeader()}>
|
|
{rows}
|
|
</LazyScroller>
|
|
);
|
|
}
|
|
|
|
const categories = lodash.map(EmojiStore.categories, cat => {
|
|
const options = {
|
|
[cat]: true,
|
|
selected: this.state.currentSection != null && this.state.currentSection.indexOf(cat) === 0,
|
|
};
|
|
|
|
return <div key={cat} className={classNames('item', options)} onClick={() => this.scrollNow(cat)} />;
|
|
});
|
|
|
|
let premiumPromo;
|
|
if (this.state.premiumPromoOpen) {
|
|
premiumPromo = (
|
|
<PremiumPromo
|
|
onLearnMore={() => PopoutActionCreators.close(this.props.popoutKey)}
|
|
onClose={() => this.setState({premiumPromoOpen: false})}
|
|
/>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="emoji-picker">
|
|
<div className={classNames('dimmer', {visible: this.state.diversityPickerOpen})} />
|
|
<div className="header">
|
|
<SearchBar
|
|
query={this.state.query}
|
|
ref="search"
|
|
light
|
|
placeholder={this.state.placeholder}
|
|
gridResults={true}
|
|
selectedRow={this.state.selectedRow}
|
|
selectedColumn={this.state.selectedColumn}
|
|
sections={this.state.metaData.map(row => row.items.length)}
|
|
onClear={this.handleClear}
|
|
onSelect={this.handleSelect}
|
|
onSelectionChange={this.handleSelectionChange}
|
|
onQueryChange={this.handleQueryChange}
|
|
/>
|
|
<DiversitySelector onOpen={this.handleDiversityPickerToggled} />
|
|
</div>
|
|
{body}
|
|
<div className="categories">
|
|
{categories}
|
|
</div>
|
|
{premiumPromo}
|
|
</div>
|
|
);
|
|
},
|
|
});
|
|
|
|
const PremiumPromo = ({onLearnMore, onClose}) => {
|
|
return (
|
|
<div className="premium-promo">
|
|
<div className="premium-promo-close" onClick={onClose}>{i18n.Messages.CLOSE}</div>
|
|
<img
|
|
className="premium-promo-image"
|
|
src={require('../images/premium/img_premium_emoji_light.svg')}
|
|
width={124}
|
|
height={96}
|
|
/>
|
|
<div className="premium-promo-title">{i18n.Messages.PREMIUM_PROMO_TITLE}</div>
|
|
<div className="premium-promo-description">{i18n.Messages.PREMIUM_PROMO_DESCRIPTION}</div>
|
|
<button
|
|
className="btn btn-primary"
|
|
onClick={() => {
|
|
UserSettingsModalActionCreators.open(UserSettingsSections.PREMIUM);
|
|
onLearnMore();
|
|
}}
|
|
type="button">
|
|
{i18n.Messages.LEARN_MORE}
|
|
</button>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const EmojiButton = React.createClass({
|
|
mixins: [PureRenderMixin],
|
|
|
|
getInitialState() {
|
|
return {
|
|
hovered: false,
|
|
uniqueId: `emoji-picker-${this.props.name}`,
|
|
};
|
|
},
|
|
|
|
componentWillMount() {
|
|
this.spriteSheetSize = [
|
|
`${EMOJI_SPRITE_SIZE * EmojiSprites.PickerPerRow}px`,
|
|
`${EMOJI_SPRITE_SIZE * Math.ceil(EmojiSprites.PickerCount / EmojiSprites.PickerPerRow)}px`,
|
|
].join(' ');
|
|
},
|
|
|
|
renderEmojiPickerPopout(props) {
|
|
return <EmojiPicker {...props} onSelectEmoji={this.props.onSelectEmoji} channel={this.props.channel} />;
|
|
},
|
|
|
|
onMouseOver() {
|
|
this.onMouseEnter();
|
|
},
|
|
|
|
onMouseEnter() {
|
|
if (this.state.hovered || PopoutStore.isOpen(this.state.uniqueId)) {
|
|
return;
|
|
}
|
|
|
|
const emojiIndex = Math.floor(Math.random() * EmojiSprites.PickerCount);
|
|
this.setState({hovered: true, emojiIndex});
|
|
},
|
|
|
|
onMouseLeave() {
|
|
this.setState({hovered: false});
|
|
},
|
|
|
|
render() {
|
|
const ABOVE_DRAG_AND_DROP = 1000;
|
|
|
|
const {emojiIndex} = this.state;
|
|
const x = -emojiIndex % EmojiSprites.PickerPerRow * EMOJI_SPRITE_SIZE;
|
|
const y = -Math.floor(emojiIndex / EmojiSprites.PickerPerRow) * EMOJI_SPRITE_SIZE;
|
|
const style = {
|
|
backgroundImage: `url('${require('../images/emoji/spritesheet-picker.png')}')`,
|
|
backgroundPosition: `${x}px ${y}px`,
|
|
backgroundSize: this.spriteSheetSize,
|
|
};
|
|
|
|
return (
|
|
<Popout
|
|
closeOnScroll={false}
|
|
render={this.renderEmojiPickerPopout}
|
|
position={Popout.TOP_RIGHT}
|
|
zIndexBoost={ABOVE_DRAG_AND_DROP}
|
|
shadow={false}
|
|
animationType="none"
|
|
uniqueId={this.state.uniqueId}
|
|
subscribeTo={ComponentActions.TOGGLE_EMOJI_POPOUT}>
|
|
<div
|
|
className={classNames('channel-textarea-emoji', {hovered: this.state.hovered})}
|
|
onMouseEnter={this.onMouseEnter}
|
|
onMouseOver={this.onMouseOver}
|
|
onMouseLeave={this.onMouseLeave}>
|
|
<div className="sprite-item" style={style} />
|
|
</div>
|
|
</Popout>
|
|
);
|
|
},
|
|
});
|
|
|
|
export default EmojiButton;
|
|
|
|
|
|
|
|
// WEBPACK FOOTER //
|
|
// ./discord_app/components/EmojiPicker.js
|