Mailspring/internal_packages/composer-emoji/lib/emoji-button-popover.jsx
Jackie Luo d0b001012e feat(composer-emoji): Add emoji button to composer
Summary: The emoji picker allows users to insert emoji to their messages on click. A few emoji that don't render properly in Chromium are replaced with PNGs, both in the composer view and the message list view, but only the Unicode characters are sent in the message bodies.

Test Plan: TODO

Reviewers: bengotow, evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D2787
2016-03-24 10:45:48 -07:00

325 lines
9.9 KiB
JavaScript

import React from 'react';
import {Actions} from 'nylas-exports';
import {RetinaImg} from 'nylas-component-kit';
import EmojiStore from './emoji-store';
import EmojiActions from './emoji-actions';
import emoji from 'node-emoji';
import categorizedEmojiList from './categorized-emoji';
import missingEmojiList from './missing-emoji';
class EmojiButtonPopover extends React.Component {
static displayName = 'EmojiButtonPopover';
constructor() {
super();
const {categoryNames,
categorizedEmoji,
categoryPositions} = this.getStateFromStore();
this.state = {
emojiName: "Emoji Picker",
categoryNames: categoryNames,
categorizedEmoji: categorizedEmoji,
categoryPositions: categoryPositions,
searchValue: "",
activeTab: Object.keys(categorizedEmoji)[0],
};
}
componentDidMount() {
this._mounted = true;
this.renderCanvas();
}
componentWillUnmount() {
this._mounted = false;
}
onMouseDown = (event) => {
const emojiName = this.calcEmojiByPosition(this.calcPosition(event));
if (!emojiName) return null;
EmojiActions.selectEmoji({emojiName, replaceSelection: false});
Actions.closePopover();
}
onScroll = () => {
const emojiContainer = document.querySelector(".emoji-finder-container");
const tabContainer = document.querySelector(".emoji-tabs");
tabContainer.className = emojiContainer.scrollTop ? "emoji-tabs shadow" : "emoji-tabs";
if (emojiContainer.scrollTop === 0) {
this.setState({activeTab: Object.keys(this.state.categorizedEmoji)[0]});
} else {
for (const category in this.state.categoryPositions) {
if (this.state.categoryPositions.hasOwnProperty(category)) {
if (emojiContainer.scrollTop >= this.state.categoryPositions[category].top &&
emojiContainer.scrollTop <= this.state.categoryPositions[category].bottom) {
if (category === 'More People') {
this.setState({activeTab: 'People'});
} else {
this.setState({activeTab: category});
}
}
}
}
}
}
onHover = (event) => {
const emojiName = this.calcEmojiByPosition(this.calcPosition(event));
if (emojiName) {
this.setState({emojiName: emojiName});
} else {
this.setState({emojiName: "Emoji Picker"});
}
}
onMouseOut = () => {
this.setState({emojiName: "Emoji Picker"});
}
onChange = (event) => {
const searchValue = event.target.value;
if (searchValue.length > 0) {
const searchMatches = this.findSearchMatches(searchValue);
this.setState({
categorizedEmoji: {
'Search Results': searchMatches,
},
categoryPositions: {
'Search Results': {
top: 25,
bottom: 25 + Math.ceil(searchMatches.length / 8) * 24,
},
},
searchValue: searchValue,
activeTab: null,
}, this.renderCanvas);
} else {
this.setState(this.getStateFromStore, () => {
this.setState({
searchValue: searchValue,
activeTab: Object.keys(this.state.categorizedEmoji)[0],
}, this.renderCanvas);
});
}
}
getStateFromStore = () => {
let categorizedEmoji = categorizedEmojiList;
const categoryPositions = {};
let categoryNames = [
'People',
'More People',
'Nature',
'Food and Drink',
'Activity',
'Travel and Places',
'Objects',
'Symbols',
'Flags',
];
const frequentlyUsedEmoji = EmojiStore.frequentlyUsedEmoji();
if (frequentlyUsedEmoji.length > 0) {
categorizedEmoji = {'Frequently Used': frequentlyUsedEmoji};
for (const category in categorizedEmojiList) {
if (categorizedEmojiList.hasOwnProperty(category)) {
categorizedEmoji[category] = categorizedEmojiList[category];
}
}
categoryNames = ["Frequently Used"].concat(categoryNames);
}
for (const name of categoryNames) {
categoryPositions[name] = {top: 0, bottom: 0};
}
let verticalPos = 25;
for (const category in categoryPositions) {
if (categoryPositions.hasOwnProperty(category)) {
const height = Math.ceil(categorizedEmoji[category].length / 8) * 24;
categoryPositions[category].top = verticalPos;
verticalPos += height;
categoryPositions[category].bottom = verticalPos;
if (category !== 'People') {
verticalPos += 24;
}
}
}
return {
categoryNames: categoryNames,
categorizedEmoji: categorizedEmoji,
categoryPositions: categoryPositions,
};
}
scrollToCategory(category) {
const container = document.querySelector(".emoji-finder-container");
if (this.state.searchValue.length > 0) {
this.setState({searchValue: ""});
this.setState(this.getStateFromStore, () => {
this.renderCanvas();
container.scrollTop = this.state.categoryPositions[category].top + 16;
});
} else {
container.scrollTop = this.state.categoryPositions[category].top + 16;
}
this.setState({activeTab: category})
}
findSearchMatches(searchValue) {
const searchMatches = [];
for (const category of Object.keys(categorizedEmojiList)) {
categorizedEmojiList[category].forEach((emojiName) => {
if (emojiName.indexOf(searchValue) !== -1) {
searchMatches.push(emojiName);
}
});
}
return searchMatches;
}
calcPosition = (event) => {
const rect = event.target.getBoundingClientRect();
const position = {
x: event.pageX - rect.left / 2,
y: event.pageY - rect.top / 2,
};
return position;
}
calcEmojiByPosition = (position) => {
for (const category in this.state.categoryPositions) {
if (this.state.categoryPositions.hasOwnProperty(category)) {
const LEFT_PADDING = 8;
const RIGHT_PADDING = 204;
const EMOJI_WIDTH = 24.5;
const EMOJI_HEIGHT = 24;
const EMOJI_PER_ROW = 8;
if (position.x >= LEFT_PADDING &&
position.x <= RIGHT_PADDING &&
position.y >= this.state.categoryPositions[category].top &&
position.y <= this.state.categoryPositions[category].bottom) {
const x = Math.round((position.x + 5) / EMOJI_WIDTH);
const y = Math.round((position.y - this.state.categoryPositions[category].top + 10) / EMOJI_HEIGHT);
const index = x + (y - 1) * EMOJI_PER_ROW - 1;
return this.state.categorizedEmoji[category][index];
}
}
}
return null;
}
renderTabs() {
const tabs = [];
this.state.categoryNames.forEach((category) => {
if (category !== 'More People') {
let className = `emoji-tab ${(category.replace(/ /g, '-')).toLowerCase()}`
if (category === this.state.activeTab) {
className += " active";
}
tabs.push(
<div key={`${category} container`} style={{flex: 1}}>
<RetinaImg
key={`${category} tab`}
className={className}
name={`icon-emojipicker-${(category.replace(/ /g, '-')).toLowerCase()}.png`}
mode={RetinaImg.Mode.ContentIsMask}
onMouseDown={() => this.scrollToCategory(category)} />
</div>
);
}
});
return tabs;
}
renderCanvas() {
const canvas = document.getElementById("emoji-canvas");
const keys = Object.keys(this.state.categoryPositions);
canvas.height = this.state.categoryPositions[keys[keys.length - 1]].bottom * 2;
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
const position = {
x: 15,
y: 45,
}
Object.keys(this.state.categorizedEmoji).forEach((category, i) => {
if (i > 0) {
setTimeout(() => this.renderCategory(category, i, ctx, position), i * 50);
} else {
this.renderCategory(category, i, ctx, position);
}
});
}
renderCategory(category, i, ctx, position) {
if (!this._mounted) return;
if (category !== "More People") {
if (i > 0) {
position.x = 18;
position.y += 48;
}
ctx.font = "24px Nylas-Pro";
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillText(category, position.x, position.y);
}
position.x = 18;
position.y += 48;
ctx.font = "32px Arial";
ctx.fillStyle = 'black';
this.state.categorizedEmoji[category].forEach((emojiName, j) => {
if (missingEmojiList.indexOf(emojiName) === -1) {
const emojiChar = emoji.get(emojiName);
ctx.fillText(emojiChar, position.x, position.y);
} else {
const img = new Image();
img.src = `images/composer-emoji/missing-emoji/${emojiName}.png`;
const x = position.x;
const y = position.y;
img.onload = () => {
ctx.drawImage(img, x, y - 30, 32, 32);
}
}
if (position.x > 325 && j < this.state.categorizedEmoji[category].length - 1) {
position.x = 18;
position.y += 48;
} else {
position.x += 50;
}
})
}
render() {
return (
<div className="emoji-button-popover" tabIndex="-1">
<div className="emoji-tabs">
{this.renderTabs()}
</div>
<div
className="emoji-finder-container"
onScroll={this.onScroll}>
<div className="emoji-search-container">
<input
type="text"
className="search"
value={this.state.searchValue}
onChange={this.onChange}
/>
</div>
<canvas
id="emoji-canvas"
width="420"
height="2000"
onMouseDown={this.onMouseDown}
onMouseOut={this.onMouseOut}
onMouseMove={this.onHover}
style={{zoom: "0.5"}}>
</canvas>
</div>
<div className="emoji-name">
{this.state.emojiName}
</div>
</div>
);
}
}
export default EmojiButtonPopover;