fix(emoji): Re-use img tag to avoid running out of file descriptors

Summary:
On my machine the new emoji picker was causing "too many open file descriptor"
errors. I think that this was because it was creating 1300 image tags in 50msec.

I refactored this code so that it uses a single image tag and only loads one image
at a time. This could make it slower on some people's machines, but eliminates the
possibility of it breaking the app!

Test Plan: Run tests

Reviewers: jackie

Differential Revision: https://phab.nylas.com/D2878
This commit is contained in:
Ben Gotow 2016-04-12 13:57:42 -07:00
parent 5e49962cfb
commit 370edc40e9
2 changed files with 64 additions and 52 deletions

View file

@ -56,9 +56,7 @@ export default categorizedEmojiList = {
'cold_sweat', 'cold_sweat',
'hushed', 'hushed',
'frowning', 'frowning',
'anguished' 'anguished',
],
'More People': [
'cry', 'cry',
'disappointed_relieved', 'disappointed_relieved',
'sleepy', 'sleepy',
@ -1310,4 +1308,4 @@ export default categorizedEmojiList = {
'flag-zm', 'flag-zm',
'flag-zw' 'flag-zw'
] ]
} }

View file

@ -27,10 +27,13 @@ class EmojiButtonPopover extends React.Component {
componentDidMount() { componentDidMount() {
this._mounted = true; this._mounted = true;
this._emojiPreloadImage = new Image();
this.renderCanvas(); this.renderCanvas();
} }
componentWillUnmount() { componentWillUnmount() {
this._emojiPreloadImage.onload = null;
this._emojiPreloadImage = null;
this._mounted = false; this._mounted = false;
} }
@ -52,11 +55,7 @@ class EmojiButtonPopover extends React.Component {
if (this.state.categoryPositions.hasOwnProperty(category)) { if (this.state.categoryPositions.hasOwnProperty(category)) {
if (emojiContainer.scrollTop >= this.state.categoryPositions[category].top && if (emojiContainer.scrollTop >= this.state.categoryPositions[category].top &&
emojiContainer.scrollTop <= this.state.categoryPositions[category].bottom) { emojiContainer.scrollTop <= this.state.categoryPositions[category].bottom) {
if (category === 'More People') { this.setState({activeTab: category});
this.setState({activeTab: 'People'});
} else {
this.setState({activeTab: category});
}
} }
} }
} }
@ -108,7 +107,6 @@ class EmojiButtonPopover extends React.Component {
const categoryPositions = {}; const categoryPositions = {};
let categoryNames = [ let categoryNames = [
'People', 'People',
'More People',
'Nature', 'Nature',
'Food and Drink', 'Food and Drink',
'Activity', 'Activity',
@ -212,22 +210,20 @@ class EmojiButtonPopover extends React.Component {
renderTabs() { renderTabs() {
const tabs = []; const tabs = [];
this.state.categoryNames.forEach((category) => { this.state.categoryNames.forEach((category) => {
if (category !== 'More People') { let className = `emoji-tab ${(category.replace(/ /g, '-')).toLowerCase()}`
let className = `emoji-tab ${(category.replace(/ /g, '-')).toLowerCase()}` if (category === this.state.activeTab) {
if (category === this.state.activeTab) { className += " active";
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>
);
} }
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; return tabs;
} }
@ -237,51 +233,69 @@ class EmojiButtonPopover extends React.Component {
const keys = Object.keys(this.state.categoryPositions); const keys = Object.keys(this.state.categoryPositions);
canvas.height = this.state.categoryPositions[keys[keys.length - 1]].bottom * 2; canvas.height = this.state.categoryPositions[keys[keys.length - 1]].bottom * 2;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
ctx.font = "24px Nylas-Pro";
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
const position = { const position = {
x: 15, x: 15,
y: 45, y: 45,
} }
Object.keys(this.state.categorizedEmoji).forEach((category, i) => {
if (i > 0) { let idx = 0;
setTimeout(() => this.renderCategory(category, i, ctx, position), i * 50); const categoryNames = Object.keys(this.state.categorizedEmoji);
} else { const renderNextCategory = () => {
this.renderCategory(category, i, ctx, position); if (!categoryNames[idx]) return;
} if (!this._mounted) return;
}); this.renderCategory(categoryNames[idx], idx, ctx, position, renderNextCategory);
idx += 1;
}
renderNextCategory();
} }
renderCategory(category, i, ctx, position) { renderCategory(category, i, ctx, position, callback) {
if (!this._mounted) return; if (i > 0) {
if (category !== "More People") { position.x = 18;
if (i > 0) { position.y += 48;
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);
} }
ctx.fillText(category, position.x, position.y);
position.x = 18; position.x = 18;
position.y += 48; position.y += 48;
ctx.font = "32px Arial";
ctx.fillStyle = 'black'; const emoji = this.state.categorizedEmoji[category];
if (this.state.categorizedEmoji[category].length === 0) return; if (!emoji || emoji.length === 0) return;
this.state.categorizedEmoji[category].forEach((emojiName, j) => {
const img = new Image(); const remaining = emoji.map((emojiName, j) => {
img.src = EmojiStore.getImagePath(emojiName);
const x = position.x; const x = position.x;
const y = position.y; const y = position.y;
img.onload = () => { const src = EmojiStore.getImagePath(emojiName);
ctx.drawImage(img, x, y - 30, 32, 32);
}
if (position.x > 325 && j < this.state.categorizedEmoji[category].length - 1) { if (position.x > 325 && j < this.state.categorizedEmoji[category].length - 1) {
position.x = 18; position.x = 18;
position.y += 48; position.y += 48;
} else { } else {
position.x += 50; position.x += 50;
} }
})
return {src, x, y};
});
const drawEmojiAt = ({src, x, y} = {}) => {
if (!src) {
return;
}
this._emojiPreloadImage.onload = () => {
this._emojiPreloadImage.onload = null;
ctx.drawImage(this._emojiPreloadImage, x, y - 30, 32, 32);
if (remaining.length === 0) {
callback();
} else {
drawEmojiAt(remaining.shift());
}
}
this._emojiPreloadImage.src = src;
}
drawEmojiAt(remaining.shift());
} }
render() { render() {