import PropTypes from "prop-types";
import { PureComponent } from "react";
import { defineMessages, injectIntl, FormattedMessage } from "react-intl";
import classNames from "classnames";
import ImmutablePropTypes from "react-immutable-proptypes";
import { supportsPassiveEvents } from "detect-passive-events";
import Overlay from "react-overlays/Overlay";
import { assetHost } from "mastodon/utils/config";
import { buildCustomEmojis, categoriesFromEmojis } from "../../emoji/emoji";
import { EmojiPicker as EmojiPickerAsync } from "../../ui/util/async-components";
const messages = defineMessages({
emoji: { id: "emoji_button.label", defaultMessage: "Insert emoji" },
emoji_search: { id: "emoji_button.search", defaultMessage: "Search..." },
custom: { id: "emoji_button.custom", defaultMessage: "Custom" },
recent: { id: "emoji_button.recent", defaultMessage: "Frequently used" },
search_results: { id: "emoji_button.search_results", defaultMessage: "Search results" },
people: { id: "emoji_button.people", defaultMessage: "People" },
nature: { id: "emoji_button.nature", defaultMessage: "Nature" },
food: { id: "emoji_button.food", defaultMessage: "Food & Drink" },
activity: { id: "emoji_button.activity", defaultMessage: "Activity" },
travel: { id: "emoji_button.travel", defaultMessage: "Travel & Places" },
objects: { id: "emoji_button.objects", defaultMessage: "Objects" },
symbols: { id: "emoji_button.symbols", defaultMessage: "Symbols" },
flags: { id: "emoji_button.flags", defaultMessage: "Flags" },
});
let EmojiPicker, Emoji; // load asynchronously
const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true;
const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`;
const notFoundFn = () => (
);
class ModifierPickerMenu extends PureComponent {
static propTypes = {
active: PropTypes.bool,
onSelect: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
};
handleClick = e => {
this.props.onSelect(e.currentTarget.getAttribute("data-index") * 1);
};
UNSAFE_componentWillReceiveProps (nextProps) {
if (nextProps.active) {
this.attachListeners();
} else {
this.removeListeners();
}
}
componentWillUnmount () {
this.removeListeners();
}
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
};
attachListeners () {
document.addEventListener("click", this.handleDocumentClick, { capture: true });
document.addEventListener("touchend", this.handleDocumentClick, listenerOptions);
}
removeListeners () {
document.removeEventListener("click", this.handleDocumentClick, { capture: true });
document.removeEventListener("touchend", this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
};
render () {
const { active } = this.props;
return (
);
}
}
class ModifierPicker extends PureComponent {
static propTypes = {
active: PropTypes.bool,
modifier: PropTypes.number,
onChange: PropTypes.func,
onClose: PropTypes.func,
onOpen: PropTypes.func,
};
handleClick = () => {
if (this.props.active) {
this.props.onClose();
} else {
this.props.onOpen();
}
};
handleSelect = modifier => {
this.props.onChange(modifier);
this.props.onClose();
};
render () {
const { active, modifier } = this.props;
return (
);
}
}
class EmojiPickerMenuImpl extends PureComponent {
static propTypes = {
custom_emojis: ImmutablePropTypes.list,
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
loading: PropTypes.bool,
onClose: PropTypes.func.isRequired,
onPick: PropTypes.func.isRequired,
style: PropTypes.object,
intl: PropTypes.object.isRequired,
skinTone: PropTypes.number.isRequired,
onSkinTone: PropTypes.func.isRequired,
};
static defaultProps = {
style: {},
loading: true,
frequentlyUsedEmojis: [],
};
state = {
modifierOpen: false,
readyToFocus: false,
};
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
};
componentDidMount () {
document.addEventListener("click", this.handleDocumentClick, { capture: true });
document.addEventListener("touchend", this.handleDocumentClick, listenerOptions);
// Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need
// to wait for a frame before focusing
requestAnimationFrame(() => {
this.setState({ readyToFocus: true });
if (this.node) {
const element = this.node.querySelector("input[type=\"search\"]");
if (element) {
element.focus();
}
}
});
}
componentWillUnmount () {
document.removeEventListener("click", this.handleDocumentClick, { capture: true });
document.removeEventListener("touchend", this.handleDocumentClick, listenerOptions);
}
setRef = c => {
this.node = c;
};
getI18n = () => {
const { intl } = this.props;
return {
search: intl.formatMessage(messages.emoji_search),
categories: {
search: intl.formatMessage(messages.search_results),
recent: intl.formatMessage(messages.recent),
people: intl.formatMessage(messages.people),
nature: intl.formatMessage(messages.nature),
foods: intl.formatMessage(messages.food),
activity: intl.formatMessage(messages.activity),
places: intl.formatMessage(messages.travel),
objects: intl.formatMessage(messages.objects),
symbols: intl.formatMessage(messages.symbols),
flags: intl.formatMessage(messages.flags),
custom: intl.formatMessage(messages.custom),
},
};
};
handleClick = (emoji, event) => {
if (!emoji.native) {
emoji.native = emoji.colons;
}
if (!(event.ctrlKey || event.metaKey)) {
this.props.onClose();
}
this.props.onPick(emoji);
};
handleModifierOpen = () => {
this.setState({ modifierOpen: true });
};
handleModifierClose = () => {
this.setState({ modifierOpen: false });
};
handleModifierChange = modifier => {
this.props.onSkinTone(modifier);
};
render () {
const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props;
if (loading) {
return ;
}
const title = intl.formatMessage(messages.emoji);
const { modifierOpen } = this.state;
const categoriesSort = [
"recent",
"people",
"nature",
"foods",
"activity",
"places",
"objects",
"symbols",
"flags",
];
categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort());
return (
);
}
}
const EmojiPickerMenu = injectIntl(EmojiPickerMenuImpl);
class EmojiPickerDropdown extends PureComponent {
static propTypes = {
custom_emojis: ImmutablePropTypes.list,
frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string),
intl: PropTypes.object.isRequired,
onPickEmoji: PropTypes.func.isRequired,
onSkinTone: PropTypes.func.isRequired,
skinTone: PropTypes.number.isRequired,
button: PropTypes.node,
};
state = {
active: false,
loading: false,
};
setRef = (c) => {
this.dropdown = c;
};
onShowDropdown = () => {
this.setState({ active: true });
if (!EmojiPicker) {
this.setState({ loading: true });
EmojiPickerAsync().then(EmojiMart => {
EmojiPicker = EmojiMart.Picker;
Emoji = EmojiMart.Emoji;
this.setState({ loading: false });
}).catch(() => {
this.setState({ loading: false, active: false });
});
}
};
onHideDropdown = () => {
this.setState({ active: false });
};
onToggle = (e) => {
if (!this.state.loading && (!e.key || e.key === "Enter")) {
if (this.state.active) {
this.onHideDropdown();
} else {
this.onShowDropdown(e);
}
}
};
handleKeyDown = e => {
if (e.key === "Escape") {
this.onHideDropdown();
}
};
setTargetRef = c => {
this.target = c;
};
findTarget = () => {
return this.target;
};
render () {
const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props;
const title = intl.formatMessage(messages.emoji);
const { active, loading } = this.state;
return (
{button ||

}
{({ props, placement })=> (
)}
);
}
}
export default injectIntl(EmojiPickerDropdown);