mirror of
https://github.com/Foundry376/Mailspring.git
synced 2024-09-23 16:56:08 +08:00
dcb8478f97
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
325 lines
9.9 KiB
JavaScript
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;
|