Fuzzy-Searchable Emoji Picker

This commit is contained in:
Ken Powers 2019-05-24 16:58:27 -07:00 committed by Scott Nonnenberg
parent 2f47a3570b
commit 0e9d549cf3
48 changed files with 1697 additions and 280 deletions

View File

@ -1835,6 +1835,25 @@
"message": "Sticker Pack",
"description": "The title that appears in the sticker pack preview modal."
},
"EmojiPicker--empty": {
"message": "No emoji found",
"description": "Shown in the emoji picker when a search yields 0 results."
},
"EmojiPicker--search-placeholder": {
"message": "Search Emoji",
"description":
"Shown as a placeholder inside the emoji picker search field."
},
"EmojiPicker--skin-tone": {
"message": "Skin tone $tone$",
"placeholders": {
"status": {
"content": "$1",
"example": "2"
}
},
"description": "Shown as a tooltip over the emoji tone buttons."
},
"confirmation-dialog--Cancel": {
"message": "Cancel",
"description": "Appears on the cancel button in confirmation dialogs."

View File

@ -123,6 +123,9 @@ module.exports = {
getAllStickers,
getRecentStickers,
updateEmojiUsage,
getRecentEmojis,
removeAll,
removeAllConfiguration,
@ -735,6 +738,29 @@ async function updateToSchemaVersion13(currentVersion, instance) {
console.log('updateToSchemaVersion13: success!');
}
async function updateToSchemaVersion14(currentVersion, instance) {
if (currentVersion >= 14) {
return;
}
console.log('updateToSchemaVersion14: starting...');
await instance.run('BEGIN TRANSACTION;');
await instance.run(`CREATE TABLE emojis(
shortName STRING PRIMARY KEY,
lastUsage INTEGER
);`);
await instance.run(`CREATE INDEX emojis_lastUsage
ON emojis (
lastUsage
);`);
await instance.run('PRAGMA schema_version = 14;');
await instance.run('COMMIT TRANSACTION;');
console.log('updateToSchemaVersion14: success!');
}
const SCHEMA_VERSIONS = [
updateToSchemaVersion1,
updateToSchemaVersion2,
@ -749,6 +775,7 @@ const SCHEMA_VERSIONS = [
updateToSchemaVersion11,
updateToSchemaVersion12,
updateToSchemaVersion13,
updateToSchemaVersion14,
];
async function updateSchema(instance) {
@ -2182,6 +2209,43 @@ async function getRecentStickers({ limit } = {}) {
return rows || [];
}
// Emojis
async function updateEmojiUsage(shortName, timeUsed = Date.now()) {
await db.run('BEGIN TRANSACTION;');
const rows = await db.get(
'SELECT * FROM emojis WHERE shortName = $shortName;',
{
$shortName: shortName,
}
);
if (rows) {
await db.run(
'UPDATE emojis SET lastUsage = $timeUsed WHERE shortName = $shortName;',
{ $shortName: shortName, $timeUsed: timeUsed }
);
} else {
await db.run(
'INSERT INTO emojis(shortName, lastUsage) VALUES ($shortName, $timeUsed);',
{ $shortName: shortName, $timeUsed: timeUsed }
);
}
await db.run('COMMIT TRANSACTION;');
}
async function getRecentEmojis(limit = 32) {
const rows = await db.all(
'SELECT * FROM emojis ORDER BY lastUsage DESC LIMIT $limit;',
{
$limit: limit,
}
);
return rows || [];
}
// All data in database
async function removeAll() {
let promise;

View File

@ -112,12 +112,11 @@
</div>
<div class='bottom-bar' id='footer'>
<div class='emoji-panel-container'></div>
<div class='attachment-list'></div>
<div class='compose'>
<form class='send clearfix file-input'>
<div class='flex'>
<button class='emoji'></button>
<div class='emoji-button-placeholder'></div>
<textarea class='send-message' placeholder='{{ send-message }}' rows='1' dir='auto'></textarea>
<div class='sticker-button-placeholder'></div>
<div class='capture-audio'>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M10,1c-5,0-9,4-9,9s4,9,9,9s9-4,9-9S15,1,10,1z M15.5,15.1c-0.1-1-0.2-2.2-0.3-3.2l-3.2-1.2c-0.9,0.7-1.7,1.4-2.5,2L10,16
c0.8,0.3,1.9,0.6,2.8,0.9c-2,0.8-4.2,0.7-6.1-0.2L9,16l-0.6-3.3c-0.8-0.6-1.7-1.2-2.6-1.8L4,12c0,0.7-0.1,1.6-0.1,2.3
C3,13.1,2.5,11.6,2.5,10c0-0.3,0-0.6,0.1-0.9L3.8,11l1.8-1l0,0c0.3-0.9,0.6-1.9,0.9-2.8L5.3,5.6L3.5,6.4C4.3,4.8,5.7,3.6,7.4,3
C7,3.6,6.5,4.4,6.1,5l1.1,1.6C8.2,6.5,9.1,6.4,10,6.4L12.4,4c-0.3-0.4-0.8-1-1.2-1.4c1.7,0.3,3.3,1.1,4.4,2.4
c-0.7-0.2-1.5-0.3-2.2-0.5l-2.4,2.4c0.4,1,0.8,1.9,1.2,2.8l3.3,1.3c0.6-0.8,1.3-1.7,1.9-2.6c0.1,0.5,0.2,1.1,0.2,1.6
C17.5,11.9,16.8,13.7,15.5,15.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 974 B

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M10,1c-5,0-9,4-9,9s4,9,9,9s9-4,9-9S15,1,10,1z M17.3,8.4c-0.6,0.9-1.3,1.8-1.9,2.6l-3.3-1.3c-0.4-0.9-0.8-1.8-1.2-2.8
l2.4-2.4C14,4.6,14.8,4.8,15.6,5C16.4,5.9,17,7.1,17.3,8.4z M11.1,2.6C11.6,3.1,12,3.5,12.4,4L10,6.4c-0.9,0-1.9,0-2.8,0.1L6.1,5
C6.5,4.3,7,3.6,7.4,3C8.2,2.7,9.1,2.5,10,2.5C10.4,2.5,10.8,2.5,11.1,2.6z M5.6,10L5.6,10l-1.8,1C3.4,10.4,3,9.7,2.6,9.1
c0.1-1,0.4-1.9,0.9-2.7l1.9-0.7l1.1,1.6C6.1,8.1,5.8,9.1,5.6,10z M3.9,14.3C3.9,13.5,4,12.7,4,12l1.8-1c0.9,0.6,1.8,1.2,2.6,1.8
L9,16l-2.3,0.7C5.6,16.1,4.6,15.3,3.9,14.3z M12.8,17c-1-0.3-1.9-0.6-2.8-0.9l-0.6-3.3c0.8-0.7,1.6-1.4,2.5-2l3.2,1.2
c0.1,1,0.2,2.1,0.3,3.2C14.7,16,13.8,16.6,12.8,17z"/>
</svg>

After

Width:  |  Height:  |  Size: 1013 B

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M16.9,9c0-0.2-0.1-0.5-0.1-0.7c0.1-0.1,0.2-0.1,0.2-0.2c1.8-1.8,2.1-4.3,0.7-5.7S13.8,1.2,12,3c-0.1,0.1-0.1,0.2-0.2,0.2
c-1.2-0.3-2.4-0.3-3.6,0C8.1,3.2,8.1,3.1,8,3C6.2,1.2,3.7,0.9,2.3,2.3S1.2,6.2,3,8l0.2,0.2C3.2,8.4,3.1,8.7,3.1,8.9
c-2.2,1.6-2.7,4.7-1.1,7C2.9,17.2,4.4,18,6,18c0,0,0.8,0,1.8-0.1c0.9,1.2,2.6,1.4,3.8,0.5c0.2-0.2,0.4-0.3,0.5-0.5
C13.1,18,14,18,14,18c2.8,0,5-2.3,4.9-5.1C18.9,11.4,18.1,9.9,16.9,9z M3.8,6.7C3.4,6.2,3.1,5.5,3,4.8C2.9,4.3,3.1,3.8,3.4,3.4
C3.7,3.1,4.1,3,4.5,3c0.8,0,1.6,0.4,2.1,0.9l0,0C5.5,4.5,4.5,5.5,3.8,6.7z M7,12.5c-0.6,0-1-0.7-1-1.5s0.4-1.5,1-1.5s1,0.7,1,1.5
S7.6,12.5,7,12.5z M11.2,16l-0.8,0.7c-0.2,0.2-0.5,0.2-0.7,0L8.8,16c-0.4-0.3-0.5-0.9-0.2-1.3c0.2-0.2,0.5-0.4,0.7-0.4h1.4
c0.5,0,0.9,0.4,0.9,0.9C11.6,15.5,11.4,15.8,11.2,16z M13,12.5c-0.6,0-1-0.7-1-1.5s0.4-1.5,1-1.5s1,0.7,1,1.5S13.6,12.5,13,12.5z
M13.3,3.8L13.3,3.8C13.9,3.3,14.7,3,15.5,3c0.4,0,0.8,0.1,1.2,0.4c0.3,0.4,0.5,0.9,0.4,1.4c-0.1,0.7-0.4,1.3-0.9,1.8
C15.5,5.5,14.5,4.5,13.3,3.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M8,11c0,0.8-0.4,1.5-1,1.5S6,11.8,6,11s0.4-1.5,1-1.5S8,10.2,8,11z M13,9.5c-0.6,0-1,0.7-1,1.5s0.4,1.5,1,1.5s1-0.7,1-1.5
S13.6,9.5,13,9.5z M19,13c0,2.8-2.2,5-5,5l0,0c-0.1,0-0.9,0-1.8-0.1c-0.9,1.2-2.6,1.4-3.8,0.5c-0.2-0.2-0.4-0.3-0.5-0.5
C6.9,18,6.1,18,6,18l0,0c-2.8,0-5-2.3-4.9-5.1c0-1.5,0.8-3,2-3.9c0-0.3,0.1-0.5,0.2-0.8C3.2,8.1,3.1,8.1,3,8
C1.2,6.2,0.9,3.7,2.3,2.3c0.6-0.6,1.4-0.9,2.2-0.9C5.9,1.5,7.1,2,8,3c0.1,0.1,0.1,0.2,0.2,0.2c1.2-0.3,2.4-0.3,3.6,0
C11.9,3.2,11.9,3.1,12,3c0.9-1,2.1-1.5,3.5-1.5c0.8,0,1.6,0.3,2.2,0.9C19.1,3.7,18.8,6.2,17,8c-0.1,0.1-0.2,0.1-0.2,0.2
c0.1,0.2,0.1,0.5,0.2,0.8C18.2,9.9,19,11.4,19,13z M13.3,3.8c1.2,0.7,2.2,1.6,2.8,2.8c0.4-0.5,0.7-1.2,0.8-1.8
c0.1-0.5-0.1-1.1-0.4-1.5C16.3,3.1,15.9,3,15.5,3C14.7,3,13.9,3.4,13.3,3.8L13.3,3.8z M3.8,6.7c0.6-1.2,1.6-2.2,2.8-2.9l0,0
C6.1,3.3,5.3,3,4.5,3C4.1,3,3.7,3.1,3.4,3.4C3.1,3.8,2.9,4.3,3,4.8C3.1,5.5,3.4,6.2,3.8,6.7z M17.5,13c0-1.1-0.6-2.2-1.5-2.8
l-0.5-0.4l-0.1-0.6c-0.4-3-3.2-5.1-6.2-4.6C6.8,4.9,4.9,6.8,4.5,9.2L4.4,9.8L4,10.2c-0.9,0.6-1.5,1.7-1.5,2.8c0,1.9,1.6,3.5,3.5,3.5
c0,0,0.8,0,1.7-0.1l0.8-0.1L9,17c0.4,0.6,1.1,0.7,1.7,0.3c0.1-0.1,0.2-0.2,0.3-0.3l0.5-0.6l0.8,0.1c0.9,0.1,1.6,0.1,1.7,0.1
C15.9,16.5,17.5,14.9,17.5,13z M11.8,14.8c0-0.6-0.4-1-1-1H9.3c-0.6,0-1,0.4-1,1c0,0.3,0.2,0.7,0.4,0.8l0.9,0.7
c0.2,0.2,0.6,0.2,0.8,0l0.9-0.7C11.6,15.4,11.8,15.1,11.8,14.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

10
images/emoji-filled-20.svg Executable file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M10,1C5.2,0.9,1.1,4.7,1,9.5C1,9.7,1,9.8,1,10c-0.1,4.8,3.7,8.9,8.5,9c0.2,0,0.3,0,0.5,0c4.8,0.1,8.9-3.7,9-8.5
c0-0.2,0-0.3,0-0.5c0.1-4.8-3.7-8.9-8.5-9C10.3,1,10.2,1,10,1z M12.8,6.8c0.8,0.1,1.3,0.8,1.2,1.6c0.1,0.8-0.5,1.5-1.2,1.6
c-0.8-0.1-1.3-0.8-1.2-1.6C11.4,7.6,12,6.9,12.8,6.8z M7.2,6.8C8,6.9,8.6,7.6,8.5,8.4C8.6,9.2,8,9.9,7.2,10C6.5,9.9,5.9,9.2,6,8.4
C5.9,7.6,6.5,6.9,7.2,6.8z M14.6,13.2c-1.9,2.5-5.5,3.1-8.1,1.1c-0.4-0.3-0.8-0.7-1.1-1.1c-0.2-0.3-0.2-0.8,0.2-1.1s0.8-0.2,1.1,0.2
c1.4,1.9,4.1,2.2,6,0.8c0.3-0.2,0.6-0.5,0.8-0.8c0.2-0.3,0.7-0.4,1.1-0.2S14.9,12.9,14.6,13.2z"/>
</svg>

After

Width:  |  Height:  |  Size: 940 B

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M17.5,3.3c-2.4-0.8-4.9-0.8-7.3,0C8.1,4,5.8,4,3.8,3.3L3.5,3.2V2H2v16h1.5v-4.2c1,0.4,2.1,0.5,3.2,0.6
c1.3,0,2.6-0.2,3.8-0.6c2.1-0.7,4.4-0.7,6.4,0l1,0.4V3.5L17.5,3.3z M5,12.6c-0.4-0.1-0.8-0.2-1.2-0.4l-0.2-0.1V4.8
C4,4.9,4.5,5.1,5,5.2V12.6z"/>
</svg>

After

Width:  |  Height:  |  Size: 602 B

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M17.5,3.3c-2.4-0.8-4.9-0.8-7.3,0C8.1,4,5.8,4,3.8,3.3L3.5,3.2V2H2v16h1.5v-4.2c1,0.4,2.1,0.5,3.2,0.6
c1.3,0,2.6-0.2,3.8-0.6c2.1-0.7,4.4-0.7,6.4,0l1,0.4V3.5L17.5,3.3z M16.5,12c-2.1-0.5-4.3-0.4-6.3,0.3c-2.1,0.7-4.4,0.7-6.4,0
l-0.2-0.1V4.8c2.3,0.7,4.8,0.7,7.1,0c1.9-0.7,4-0.7,5.9-0.2V12z"/>
</svg>

After

Width:  |  Height:  |  Size: 648 B

13
images/emoji-food-filled-20.svg Executable file
View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M9.1,10.4C8.7,9.7,8.5,9,8.5,8.2c0-0.8,0.2-1.6,0.6-2.3c0.3-0.5,0.4-1.1,0.4-1.8c0-0.6-0.2-1.2-0.4-1.7l1.4-0.6
C10.8,2.6,11,3.4,11,4.2c0,0.8-0.2,1.6-0.6,2.3C10.2,7,10,7.6,10,8.2c0,0.6,0.2,1.1,0.4,1.6L9.1,10.4z M13.4,9.8
C13.2,9.3,13,8.8,13,8.2c0-0.6,0.2-1.2,0.4-1.7C13.8,5.8,14,5,14,4.2c0-0.8-0.2-1.6-0.6-2.3l-1.4,0.6c0.3,0.5,0.4,1.1,0.4,1.7
c0,0.6-0.2,1.2-0.4,1.8c-0.4,0.7-0.5,1.5-0.6,2.3c0,0.8,0.2,1.6,0.6,2.3L13.4,9.8z M19,10.3L19,10.3c0,2.7-2.3,8.7-9,8.7
c-4.9,0-8.8-3.8-9-8.7l0,0C1,9.1,2.8,8,5.6,7.4c0.1-0.5,0.3-1,0.5-1.5c0.3-0.5,0.4-1.1,0.4-1.8c0-0.6-0.2-1.2-0.4-1.7l1.4-0.6
C7.8,2.6,8,3.4,8,4.2C8,5,7.8,5.8,7.4,6.5C7.2,7,7,7.6,7,8.2c0,0.2,0,0.3,0,0.5c0.1,0.4,0.2,0.8,0.4,1.2L6,10.4
C5.8,10,5.7,9.5,5.6,9c-1.1,0.2-2.2,0.6-3,1.4c0.4,0.6,2.9,1.9,7.5,1.9s7.1-1.3,7.5-1.9c-0.8-0.7-1.8-1.2-2.9-1.4
c-0.1-0.3-0.1-0.5-0.1-0.8c0-0.2,0-0.5,0.1-0.7C17.2,8,19,9.1,19,10.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M12.1,10.4c-0.4-0.7-0.6-1.5-0.6-2.3c0-0.8,0.2-1.6,0.6-2.3c0.3-0.5,0.4-1.1,0.4-1.8c0-0.6-0.2-1.2-0.4-1.7l1.4-0.6
C13.8,2.6,14,3.4,14,4.2c0,0.8-0.2,1.6-0.6,2.3C13.2,7,13,7.6,13,8.2c0,0.6,0.2,1.1,0.4,1.6L12.1,10.4z M10.4,9.8
C10.2,9.3,10,8.8,10,8.2c0-0.6,0.2-1.2,0.4-1.7C10.8,5.8,11,5,11,4.2c0-0.8-0.2-1.6-0.6-2.3L9.1,2.5C9.3,3,9.5,3.6,9.5,4.2
c0,0.6-0.2,1.2-0.4,1.8C8.7,6.6,8.5,7.4,8.5,8.2c0,0.8,0.2,1.6,0.6,2.3L10.4,9.8z M19,10.3L19,10.3c0,2.7-2.3,8.7-9,8.7
c-4.9,0-8.8-3.8-9-8.7l0,0C1,9.1,2.8,8,5.6,7.4c0.1-0.5,0.3-1,0.5-1.5c0.3-0.5,0.4-1.1,0.4-1.8c0-0.6-0.2-1.2-0.4-1.7l1.4-0.6
C7.8,2.6,8,3.4,8,4.2C8,5,7.8,5.8,7.4,6.5C7.2,7,7,7.6,7,8.2c0,0.2,0,0.3,0,0.5c0.1,0.4,0.2,0.8,0.4,1.2L6,10.4
C5.8,10,5.7,9.5,5.6,9c-1.1,0.2-2.2,0.6-3,1.4c0.4,0.6,2.9,1.9,7.5,1.9s7.1-1.3,7.5-1.9c-0.8-0.7-1.8-1.2-2.9-1.4
c-0.1-0.3-0.1-0.5-0.1-0.8c0-0.2,0-0.5,0.1-0.7C17.2,8,19,9.1,19,10.3z M17.2,12c-0.2,0.3-0.5,0.5-0.8,0.6c-2.1,0.7-4.2,1-6.4,1
c-2.2,0-4.3-0.3-6.3-1C3.3,12.5,3,12.3,2.8,12c0.1,0.4,0.2,0.7,0.4,1.1c1.1,2.8,3.8,4.5,6.8,4.5c3,0.1,5.7-1.7,6.9-4.5
C17,12.7,17.1,12.4,17.2,12z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M16.5,7.5C16.5,3.9,13.6,1,10,1C6.4,1,3.5,3.9,3.5,7.5c0,1.5,0.5,2.9,1.4,4.1l0,0l0,0v0.1c1.4,1.6,2.6,2.9,2.6,4.4v1.5
c0,0.6,0.4,1.2,1,1.5h3c0.6-0.3,1-0.9,1-1.5H9V16h3.5c0-1.5,1.2-2.8,2.5-4.3v-0.1l0,0l0,0C16,10.5,16.5,9,16.5,7.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 591 B

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M10,1C6.4,1,3.5,3.9,3.5,7.5c0,1.5,0.5,2.9,1.4,4.1c1.3,1.7,2.6,2.9,2.6,4.4v1.5c0,0.6,0.4,1.2,1,1.5h3c0.6-0.3,1-0.9,1-1.5
H9V16h3.5c0-1.5,1.2-2.8,2.6-4.4c2.3-2.8,1.8-6.9-1-9.1C12.9,1.5,11.5,1,10,1z M8.7,14.5c-0.5-1.2-1.2-2.2-2.1-3.2l-0.5-0.7
C5.4,9.8,5,8.6,5,7.5c0-2.8,2.2-5,5-5s5,2.2,5,5c0,1.2-0.4,2.3-1.1,3.2l-0.5,0.6c-0.9,0.9-1.6,2-2.1,3.2H8.7z"/>
</svg>

After

Width:  |  Height:  |  Size: 711 B

12
images/emoji-outline-20.svg Executable file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M10,15.5c-1.8,0-3.5-0.8-4.6-2.3c-0.2-0.3-0.2-0.8,0.2-1.1c0.3-0.2,0.8-0.2,1.1,0.2c1.4,1.9,4.1,2.2,6,0.8
c0.3-0.2,0.6-0.5,0.8-0.8c0.2-0.3,0.7-0.4,1.1-0.2s0.4,0.7,0.2,1.1l0,0C13.5,14.7,11.8,15.5,10,15.5z M10,2.5c-4-0.1-7.4,3-7.5,7
c0,0.2,0,0.3,0,0.5c-0.1,4,3,7.4,7,7.5c0.2,0,0.3,0,0.5,0c4,0.1,7.4-3,7.5-7c0-0.2,0-0.3,0-0.5c0.1-4-3-7.4-7-7.5
C10.3,2.5,10.2,2.5,10,2.5 M10,1c4.8-0.1,8.9,3.7,9,8.5c0,0.2,0,0.3,0,0.5c0.1,4.8-3.7,8.9-8.5,9c-0.2,0-0.3,0-0.5,0
c-4.8,0.1-8.9-3.7-9-8.5c0-0.2,0-0.3,0-0.5C0.9,5.2,4.7,1.1,9.5,1C9.7,1,9.8,1,10,1z M7.2,6.8C6.5,6.9,5.9,7.6,6,8.4
C5.9,9.2,6.5,9.9,7.2,10C8,9.9,8.6,9.2,8.5,8.4C8.6,7.6,8,6.9,7.2,6.8z M12.8,6.8c-0.8,0.1-1.3,0.8-1.2,1.6C11.4,9.2,12,9.9,12.8,10
c0.8-0.1,1.3-0.8,1.2-1.6C14.1,7.6,13.5,6.9,12.8,6.8z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M18.9,5.8c-0.2-0.9-0.6-1.7-1.3-2.4c-1.9-1.9-4.9-1.9-6.7,0c-0.3,0.3-0.6,0.7-0.9,1c-0.3-0.4-0.6-0.7-0.9-1
c-1.9-1.9-4.9-1.9-6.7,0C1.7,4,1.3,4.8,1.1,5.7C1,6,1,6.3,1,6.7c0,4.6,5.1,9.1,9,12.4c4-3.2,9-7.8,9-12.4C19,6.4,19,6.1,18.9,5.8z"
/>
</svg>

After

Width:  |  Height:  |  Size: 596 B

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M14.2,3.5c1.6,0,2.9,1.1,3.2,2.6c0,0.2,0.1,0.4,0.1,0.5c0,3.6-4,7.5-7.5,10.4c-2.8-2.3-7.5-6.6-7.5-10.4
c0-0.2,0-0.4,0.1-0.6l0,0C2.7,5.5,3,4.9,3.4,4.5c1.3-1.3,3.3-1.3,4.6,0l0,0l0,0C8.3,4.7,8.5,5,8.7,5.3L10,7.1l1.2-1.9
c0.2-0.3,0.4-0.5,0.6-0.8l0,0l0,0l0,0C12.5,3.9,13.3,3.5,14.2,3.5 M14.2,2c-1.3,0-2.5,0.5-3.4,1.4c-0.3,0.3-0.6,0.7-0.8,1
c-0.3-0.4-0.6-0.7-0.9-1c-1.9-1.9-4.9-1.9-6.7,0C1.7,4,1.3,4.8,1.1,5.7C1,6,1,6.3,1,6.7c0,4.6,5.1,9.1,9,12.4c4-3.2,9-7.8,9-12.4
c0-0.3,0-0.6-0.1-0.9C18.4,3.6,16.5,2,14.2,2L14.2,2z"/>
</svg>

After

Width:  |  Height:  |  Size: 877 B

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M14.5,8.5l2.2-2.2c1.6-1.6,2.2-3.6,1.4-4.4s-2.9-0.1-4.4,1.4l-2.2,2.2L7.7,4.2l-4.8-2L1.1,4l4.5,3.9l1.7,1.7l-1.2,1.2
c-0.2,0.3-0.4,0.5-0.6,0.9H2.1l-1,1.1l3.1,1.9c-0.2,0.5-0.3,0.9-0.3,0.9l0.6,0.6l0.9-0.3l1.9,3l1-1v-3.4c0.3-0.2,0.6-0.4,0.9-0.6
l1.1-1.1l1.7,1.8l3.8,4.6l1.8-1.8l-1.9-4.9L14.5,8.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 656 B

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M16,8.2L17.2,7c1.6-1.6,1.9-3.9,0.8-5c-0.5-0.4-1.1-0.7-1.8-0.7c-1.2,0-2.4,0.5-3.2,1.4L11.7,4L8.3,3.6L3.2,1.9L0.9,4.2
L5.5,8l1.1,1.1l-1.2,1.2c-0.3,0.4-0.5,0.8-0.7,1.2l-2.6,0.2l-1.2,1.2l2.9,1.7c-0.1,0.6-0.2,1.1-0.2,1.1l0.8,0.8l1-0.2L7,19l1.2-1.2
l0.2-2.5c0.4-0.2,0.8-0.4,1.2-0.7l1.2-1.2l1.1,1.1l3.7,4.7l2.3-2.3l-1.5-5.3L16,8.2z M6.5,6.9l-3.6-3l0.5-0.5L8,5.1l2.5,0.2L7.6,8.1
L6.5,6.9z M8.1,13.9c-1,0.6-2.1,1-3.2,1.2C5.1,14,5.5,12.8,6,11.8l8-8c0.6-0.6,1.3-1,2.2-1c0.3,0,0.5,0.1,0.7,0.2
c0.4,0.4,0.3,1.8-0.8,2.9L8.1,13.9z M15.9,17L13,13.4l-1.1-1.1l2.8-2.8l0.2,2.5l1.5,4.7L15.9,17z"/>
</svg>

After

Width:  |  Height:  |  Size: 942 B

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Export" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 20 20" style="enable-background:new 0 0 20 20;" xml:space="preserve">
<path d="M10,2.5c4.1,0,7.5,3.4,7.5,7.5s-3.4,7.5-7.5,7.5S2.5,14.1,2.5,10S5.9,2.5,10,2.5 M10,1c-5,0-9,4-9,9s4,9,9,9s9-4,9-9
S15,1,10,1z M11,4h-1L9.6,9.6L5,10v1l5.5,0.5c0.6,0,1-0.4,1-1L11,4z"/>
</svg>

After

Width:  |  Height:  |  Size: 543 B

View File

@ -434,6 +434,7 @@
await Promise.all([
ConversationController.load(),
Signal.Stickers.load(),
Signal.Emojis.load(),
textsecure.storage.protocol.hydrateCaches(),
]);
} catch (error) {
@ -457,6 +458,7 @@
conversations: {
conversationLookup: Signal.Util.makeLookup(conversations, 'id'),
},
emojis: Signal.Emojis.getInitialState(),
items: storage.getItemsState(),
stickers: Signal.Stickers.getInitialState(),
user: {
@ -480,6 +482,10 @@
Signal.State.Ducks.conversations.actions,
store.dispatch
);
actions.emojis = Signal.State.bindActionCreators(
Signal.State.Ducks.emojis.actions,
store.dispatch
);
actions.items = Signal.State.bindActionCreators(
Signal.State.Ducks.items.actions,
store.dispatch

View File

@ -18,3 +18,8 @@ export function getRecentStickers(): Promise<
packId: string;
}>
>;
export function updateEmojiUsage(shortName: string): Promise<void>;
export function getRecentEmojis(
limit: number
): Promise<Array<{ shortName: string; lastUsage: string }>>;

View File

@ -150,6 +150,9 @@ module.exports = {
getAllStickers,
getRecentStickers,
updateEmojiUsage,
getRecentEmojis,
removeAll,
removeAllConfiguration,
@ -934,6 +937,14 @@ async function getRecentStickers() {
return recentStickers;
}
// Emojis
async function updateEmojiUsage(shortName) {
await channels.updateEmojiUsage(shortName);
}
async function getRecentEmojis(limit = 32) {
return channels.getRecentEmojis(limit);
}
// Other
async function removeAll() {

28
js/modules/emojis.js Normal file
View File

@ -0,0 +1,28 @@
const { take } = require('lodash');
const { getRecentEmojis } = require('./data');
const { replaceColons } = require('../../ts/components/emoji/lib');
module.exports = {
getInitialState,
load,
replaceColons,
};
let initialState = null;
async function load() {
const recents = await getRecentEmojisForRedux();
initialState = {
recents: take(recents, 32),
};
}
async function getRecentEmojisForRedux() {
const recent = await getRecentEmojis();
return recent.map(e => e.shortName);
}
function getInitialState() {
return initialState;
}

View File

@ -5,6 +5,7 @@ const Backbone = require('../../ts/backbone');
const Crypto = require('./crypto');
const Data = require('./data');
const Database = require('./database');
const Emojis = require('./emojis');
const Emoji = require('../../ts/util/emoji');
const IndexedDB = require('./indexeddb');
const Notifications = require('../../ts/notifications');
@ -69,6 +70,7 @@ const {
} = require('../../ts/components/conversation/VerificationNotification');
// State
const { createEmojiButton } = require('../../ts/state/roots/createEmojiButton');
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
const {
createStickerButton,
@ -82,6 +84,7 @@ const {
const { createStore } = require('../../ts/state/createStore');
const conversationsDuck = require('../../ts/state/ducks/conversations');
const emojisDuck = require('../../ts/state/ducks/emojis');
const itemsDuck = require('../../ts/state/ducks/items');
const stickersDuck = require('../../ts/state/ducks/stickers');
const userDuck = require('../../ts/state/ducks/user');
@ -262,6 +265,7 @@ exports.setup = (options = {}) => {
};
const Roots = {
createEmojiButton,
createLeftPane,
createStickerButton,
createStickerManager,
@ -269,6 +273,7 @@ exports.setup = (options = {}) => {
};
const Ducks = {
conversations: conversationsDuck,
emojis: emojisDuck,
items: itemsDuck,
user: userDuck,
stickers: stickersDuck,
@ -308,6 +313,7 @@ exports.setup = (options = {}) => {
Crypto,
Data,
Database,
Emojis,
Emoji,
IndexedDB,
LinkPreviews,

View File

@ -2,8 +2,6 @@
$,
_,
ConversationController
emojiData,
EmojiPanel,
extension,
i18n,
Signal,
@ -278,23 +276,21 @@
this.$('.send-message').focus(this.focusBottomBar.bind(this));
this.$('.send-message').blur(this.unfocusBottomBar.bind(this));
this.$emojiPanelContainer = this.$('.emoji-panel-container');
this.setupEmojiPickerButton();
this.setupStickerPickerButton();
},
events: {
keydown: 'onKeyDown',
'submit .send': 'clickSend',
'input .send-message': 'updateMessageFieldSize',
'keydown .send-message': 'updateMessageFieldSize',
'keyup .send-message': 'onKeyUp',
click: 'onClick',
'click .sticker-button-placeholder': 'onClickStickerButtonPlaceholder',
'click .emoji-button-placeholder': 'onClickPlaceholder',
'click .sticker-button-placeholder': 'onClickPlaceholder',
'click .bottom-bar': 'focusMessageField',
'click .capture-audio .microphone': 'captureAudio',
'click .module-scroll-down': 'scrollToBottom',
'click button.emoji': 'toggleEmojiPanel',
'focus .send-message': 'focusBottomBar',
'change .file-input': 'toggleMicrophone',
'blur .send-message': 'unfocusBottomBar',
@ -314,6 +310,20 @@
paste: 'onPaste',
},
setupEmojiPickerButton() {
const props = {
onPickEmoji: e => this.insertEmoji(e),
};
this.emojiButtonView = new Whisper.ReactWrapperView({
className: 'emoji-button-wrapper',
JSX: Signal.State.Roots.createEmojiButton(window.reduxStore, props),
});
// Finally, add it to the DOM
this.$('.emoji-button-placeholder').append(this.emojiButtonView.el);
},
setupStickerPickerButton() {
if (!window.ENABLE_STICKER_SEND) {
return;
@ -334,9 +344,9 @@
this.$('.sticker-button-placeholder').append(this.stickerButtonView.el);
},
// We need this, or clicking the sticker button will submit the form and send any
// We need this, or clicking the reactified buttons will submit the form and send any
// mid-composition message content.
onClickStickerButtonPlaceholder(e) {
onClickPlaceholder(e) {
e.preventDefault();
},
@ -1684,40 +1694,10 @@
return null;
},
toggleEmojiPanel(e) {
e.preventDefault();
if (!this.emojiPanel) {
this.openEmojiPanel();
} else {
this.closeEmojiPanel();
}
},
onKeyDown(event) {
if (event.key !== 'Escape') {
return;
}
this.closeEmojiPanel();
},
openEmojiPanel() {
this.$emojiPanelContainer.outerHeight(200);
this.emojiPanel = new EmojiPanel(this.$emojiPanelContainer[0], {
onClick: this.insertEmoji.bind(this),
});
this.view.resetScrollPosition();
this.updateMessageFieldSize({});
},
closeEmojiPanel() {
if (this.emojiPanel === null) {
return;
}
this.$emojiPanelContainer.empty().outerHeight(0);
this.emojiPanel = null;
this.view.resetScrollPosition();
this.updateMessageFieldSize({});
},
insertEmoji(e) {
const colons = `:${emojiData[e.index].short_name}:`;
insertEmoji({ shortName, skinTone }) {
const colons = `:${shortName}:${
skinTone ? `:skin-tone-${skinTone}:` : ''
}`;
const textarea = this.$messageField[0];
if (textarea.selectionStart || textarea.selectionStart === 0) {
@ -1806,11 +1786,10 @@
async sendMessage(e) {
this.removeLastSeenIndicator();
this.closeEmojiPanel();
this.model.clearTypingTimers();
const input = this.$messageField;
const message = window.Signal.Emoji.replaceColons(input.val()).trim();
const message = window.Signal.Emojis.replaceColons(input.val()).trim();
let toast;
if (extension.expired()) {
@ -2283,7 +2262,6 @@
const height =
this.$messageField.outerHeight() +
$attachmentPreviews.outerHeight() +
this.$emojiPanelContainer.outerHeight() +
quoteHeight +
parseInt($bottomBar.css('min-height'), 10);

View File

@ -58,11 +58,11 @@
"emoji-datasource": "4.0.0",
"emoji-datasource-apple": "4.0.0",
"emoji-js": "3.4.0",
"emoji-panel": "https://github.com/scottnonnenberg-signal/emoji-panel.git#v0.5.5",
"filesize": "3.6.1",
"firstline": "1.2.1",
"form-data": "2.3.2",
"fs-extra": "5.0.0",
"fuse.js": "^3.4.4",
"glob": "7.1.2",
"google-libphonenumber": "3.2.2",
"got": "8.2.0",
@ -209,11 +209,16 @@
"target": [
"nsis"
],
"extraFiles": [{
"from": "node_modules/@journeyapps/sqlcipher/build/Release/",
"to": ".",
"filter": ["msvcp140.dll", "vcruntime140.dll"]
}]
"extraFiles": [
{
"from": "node_modules/@journeyapps/sqlcipher/build/Release/",
"to": ".",
"filter": [
"msvcp140.dll",
"vcruntime140.dll"
]
}
]
},
"nsis": {
"deleteAppDataOnUninstall": true
@ -268,11 +273,6 @@
"fonts/*",
"build/assets",
"node_modules/**",
"!node_modules/emoji-panel/dist/*",
"!node_modules/emoji-panel/lib/emoji-panel-emojione-*.css",
"!node_modules/emoji-panel/lib/emoji-panel-google-*.css",
"!node_modules/emoji-panel/lib/emoji-panel-twitter-*.css",
"!node_modules/emoji-panel/lib/emoji-panel-apple-{16,20,64}.css",
"!node_modules/emoji-datasource/emoji_pretty.json",
"!node_modules/emoji-datasource/*.png",
"!node_modules/emoji-datasource-apple/emoji_pretty.json",

View File

@ -278,7 +278,6 @@ const { autoOrientImage } = require('./js/modules/auto_orient_image');
window.autoOrientImage = autoOrientImage;
window.dataURLToBlobSync = require('blueimp-canvas-to-blob');
window.emojiData = require('emoji-datasource');
window.EmojiPanel = require('emoji-panel');
window.filesize = require('filesize');
window.libphonenumber = require('google-libphonenumber').PhoneNumberUtil.getInstance();
window.libphonenumber.PhoneNumberFormat = require('google-libphonenumber').PhoneNumberFormat;

View File

@ -16,6 +16,11 @@ module.exports = {
description: 'Everything necessary to render a conversation',
components: 'ts/components/conversation/[^_]*.tsx',
},
{
name: 'Emoji',
description: 'All components related to emojis',
components: 'ts/components/emoji/[^_]*.tsx',
},
{
name: 'Media Gallery',
description: 'Display media and documents in a conversation',

View File

@ -80,67 +80,3 @@ img.emoji.jumbo {
width: 1em;
height: 1em;
}
button.emoji {
width: 36px;
height: 36px;
padding: 0;
opacity: 0.5;
border: none;
background: transparent;
margin-top: 3px;
&:before {
content: '';
display: inline-block;
width: $button-height;
height: $button-height;
@include color-svg('../images/smile.svg', $grey);
}
&:focus,
&:hover {
opacity: 1;
}
}
// Import emoji panel css and override paths
@import '../node_modules/emoji-panel/lib/emoji-panel-apple-32.css';
@font-face {
font-family: 'apple-category';
src: url(../node_modules/emoji-panel/lib/asset/apple.ttf) format('truetype');
font-weight: normal;
font-style: normal;
}
.emoji-panel-container {
height: 0px;
.ep-emojies {
background-color: $color-white;
}
.ep-categories {
background-color: $color-light-10;
margin-bottom: 6px;
}
.ep-e {
background-image: url('../node_modules/emoji-datasource-apple/img/apple/sheets/64.png');
background-size: 1734px;
}
.ep-slide {
background-color: $blue;
}
.ep ::-webkit-scrollbar {
// matches what is set in _global.scss; needs !important to override emoji panel CSS
width: 9px !important;
}
.ep ::-webkit-scrollbar-thumb {
background: $color-light-35;
&:hover {
background: $color-light-45;
}
}
}

View File

@ -18,16 +18,14 @@ body {
::-webkit-scrollbar {
width: 9px;
height: 9px;
}
::-webkit-scrollbar-track {
background: $color-white;
background: transparent;
}
::-webkit-scrollbar-thumb {
background: $color-light-35;
border: 2px solid $color-white;
&:hover {
background: $color-light-45;

View File

@ -3173,31 +3173,43 @@
outline: none;
}
// Module: StickerPicker
// Module: CompositionPopper
.module-sticker-picker {
%module-composition-popper {
width: 332px;
height: 400px;
border-radius: 8px;
display: grid;
grid-template-rows: 44px 1fr;
grid-template-columns: 1fr;
margin-bottom: 6px;
z-index: 2;
user-select: none;
overflow: hidden;
z-index: 2;
margin-bottom: 6px;
@include popper-shadow();
@include light-theme {
background: $color-gray-02;
::-webkit-scrollbar-thumb {
border: 2px solid $color-gray-02;
}
}
@include dark-theme {
background: $color-gray-75;
::-webkit-scrollbar-thumb {
border: 2px solid $color-gray-75;
}
}
}
// Module: StickerPicker
.module-sticker-picker {
@extend %module-composition-popper;
height: 400px;
display: grid;
grid-template-rows: 44px 1fr;
grid-template-columns: 1fr;
}
.module-sticker-picker__header {
display: flex;
flex-direction: row;
@ -3830,6 +3842,7 @@
content: '';
width: 24px;
height: 24px;
flex-shrink: 0;
@include light-theme {
@include color-svg('../images/sticker-filled.svg', $color-gray-60);
@ -4077,6 +4090,320 @@
}
}
// Module: Emoji Picker
%module-emoji-picker--ribbon {
height: 44px;
display: flex;
flex-direction: row;
align-items: center;
}
.module-emoji-picker {
@extend %module-composition-popper;
height: 428px;
display: grid;
grid-template-rows: 44px 1fr;
grid-template-columns: 1fr;
&__header {
@extend %module-emoji-picker--ribbon;
justify-content: space-between;
margin: 0 12px;
&__search-field {
flex-grow: 1;
margin-left: 8px;
position: relative;
&::after {
display: block;
content: '';
width: 16px;
height: 16px;
position: absolute;
left: 8px;
top: 6px;
@include light-theme {
@include color-svg('../images/search.svg', $color-gray-60);
}
@include dark-theme {
@include color-svg('../images/search.svg', $color-gray-25);
}
}
&__input {
width: 100%;
height: 28px;
line-height: 28px;
font-size: 14px;
border-radius: 17px;
border-width: 1px;
border-style: solid;
padding: 0 8px 0 30px;
&:focus {
outline: none;
}
@include light-theme {
background: $color-white;
color: $color-gray-90;
border-color: $color-gray-60;
&:focus {
border-color: $color-signal-blue;
}
&::placeholder {
color: $color-gray-45;
}
}
@include dark-theme {
border-color: $color-gray-25;
background: $color-gray-75;
color: $color-gray-05;
&:focus {
border-color: $color-signal-blue;
}
&::placeholder {
color: $color-gray-45;
}
}
}
}
}
&__footer {
@extend %module-emoji-picker--ribbon;
justify-content: center;
}
&__button {
width: 28px;
height: 28px;
border: none;
border-radius: 8px;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
background: none;
&--footer {
&:not(:first-of-type) {
margin-left: 4px;
}
}
&--selected {
@include light-theme {
background: $color-gray-10;
}
@include dark-theme {
background: $color-gray-60;
}
}
&--icon {
display: flex;
justify-content: center;
align-items: center;
&::after {
display: block;
content: '';
width: 20px;
height: 20px;
}
&--search {
&::after {
@include light-theme {
@include color-svg('../images/search.svg', $color-gray-60);
}
@include dark-theme {
@include color-svg('../images/search.svg', $color-gray-25);
}
}
}
&--close {
&::after {
@include light-theme {
@include color-svg('../images/x.svg', $color-gray-60);
}
@include dark-theme {
@include color-svg('../images/x.svg', $color-gray-25);
}
}
}
&--recents {
&::after {
@include light-theme {
@include color-svg(
'../images/recent-outline-20.svg',
$color-gray-60
);
}
@include dark-theme {
@include color-svg(
'../images/recent-outline-20.svg',
$color-gray-25
);
}
}
}
&--emoji {
&::after {
@include light-theme {
@include color-svg(
'../images/emoji-outline-20.svg',
$color-gray-60
);
}
@include dark-theme {
@include color-svg('../images/emoji-filled-20.svg', $color-gray-25);
}
}
}
$categories: animal food activity travel object symbol flag;
@each $cat in $categories {
&--#{$cat} {
&::after {
@include light-theme {
@include color-svg(
'../images/emoji-#{$cat}-outline-20.svg',
$color-gray-60
);
}
@include dark-theme {
@include color-svg(
'../images/emoji-#{$cat}-filled-20.svg',
$color-gray-25
);
}
}
}
}
}
}
&__body {
padding: 8px 16px 0 12px;
&__emoji-cell {
display: flex;
flex-direction: row;
justify-content: center;
align-items: flex-start;
}
&--empty {
display: flex;
padding: 0;
justify-content: center;
align-items: center;
@include light-theme {
color: $color-gray-60;
}
@include dark-theme {
color: $color-gray-25;
}
}
}
}
// Module: EmojiButton
.emoji-button-wrapper {
height: 36px;
display: flex;
justify-content: center;
align-items: center;
margin: 0 6px;
padding-top: 4px;
}
.module-emoji-button__button {
border: 0;
background: none;
width: 32px;
height: 32px;
border-radius: 16px;
display: flex;
justify-content: center;
align-items: center;
opacity: 0.5;
&:hover {
opacity: 1;
}
&:after {
display: block;
content: '';
width: 24px;
height: 24px;
flex-shrink: 0;
@include light-theme {
@include color-svg('../images/smile.svg', $color-gray-60);
}
@include dark-theme {
@include color-svg('../images/smile.svg', $color-gray-25);
}
}
&--active {
@include light-theme() {
background: $color-gray-10;
}
@include dark-theme() {
background: $color-gray-75;
}
opacity: 1;
}
}
// Module: Emoji
@mixin emoji-size($size, $emoji-sheet-columns: 51) {
&--#{$size} {
width: $size;
height: $size;
background-size: $emoji-sheet-columns * $size;
}
}
.module-emoji {
display: block;
background-image: url('../node_modules/emoji-datasource-apple/img/apple/sheets-256/64.png');
@include emoji-size(16px);
@include emoji-size(20px);
@include emoji-size(28px);
@include emoji-size(32px);
@include emoji-size(64px);
@include emoji-size(66px);
&--inline {
display: inline-block;
}
}
// Third-party module: react-contextmenu
.react-contextmenu {

View File

@ -164,32 +164,15 @@ body.dark-theme {
background-color: $color-dark-85;
}
button.emoji {
&:before {
margin-top: 4px;
@include color-svg('../images/smile.svg', $color-dark-30);
}
}
.emoji-panel-container {
.ep ::-webkit-scrollbar-thumb {
background: $color-dark-55;
&:hover {
background: $color-dark-30;
}
}
}
// _global
::-webkit-scrollbar-track {
background: $color-black;
background: transparent;
}
::-webkit-scrollbar-thumb {
background: $color-dark-55;
border: 2px solid $color-black;
border: 2px solid $color-dark-85;
&:hover {
background: $color-dark-30;
@ -398,10 +381,6 @@ body.dark-theme {
::-webkit-scrollbar-track {
background: $color-dark-85;
}
::-webkit-scrollbar-thumb {
border: 2px solid $color-dark-85;
}
}
.network-status-container {
.network-status {

View File

@ -0,0 +1,21 @@
#### Simple Emoji
```jsx
<div>
<Emoji shortName="grinning_face_with_star_eyes" />
<Emoji shortName="grinning_face_with_star_eyes" size={64} />
</div>
```
#### More Options
```jsx
<div>
<Emoji inline shortName="raised_back_of_hand" />
<Emoji inline shortName="raised_back_of_hand" skinTone={1} />
<Emoji inline shortName="raised_back_of_hand" skinTone={2} />
<Emoji inline shortName="raised_back_of_hand" skinTone={3} />
<Emoji inline shortName="raised_back_of_hand" skinTone={4} />
<Emoji inline shortName="raised_back_of_hand" skinTone={5} />
</div>
```

View File

@ -0,0 +1,43 @@
import * as React from 'react';
import classNames from 'classnames';
import { getSheetCoordinates, SkinToneKey } from './lib';
export type OwnProps = {
inline?: boolean;
shortName: string;
skinTone?: SkinToneKey | number;
size?: 16 | 20 | 28 | 32 | 64 | 66;
};
export type Props = OwnProps &
Pick<React.HTMLProps<HTMLDivElement>, 'style' | 'className'>;
export const Emoji = React.memo(
React.forwardRef<HTMLDivElement, Props>(
(
{ style = {}, size = 28, shortName, skinTone, inline, className }: Props,
ref
) => {
const [sheetX, sheetY] = getSheetCoordinates(shortName, skinTone);
const x = -(size * sheetX);
const y = -(size * sheetY);
return (
<div
ref={ref}
className={classNames(
'module-emoji',
`module-emoji--${size}px`,
inline ? 'module-emoji--inline' : null,
className
)}
style={{
...style,
backgroundPositionX: `${x}px`,
backgroundPositionY: `${y}px`,
}}
/>
);
}
)
);

View File

@ -0,0 +1,56 @@
#### Default
```jsx
<util.ConversationContext theme={util.theme}>
<div
style={{
height: '500px',
display: 'flex',
flexDirection: 'column',
justifyContent: 'flex-end',
}}
>
<EmojiButton
i18n={util.i18n}
onPickEmoji={e => console.log('onPickEmoji', e)}
skinTone={0}
onSetSkinTone={t => console.log('onSetSkinTone', t)}
onClose={() => console.log('onClose')}
recentEmojis={[
'grinning',
'grin',
'joy',
'rolling_on_the_floor_laughing',
'smiley',
'smile',
'sweat_smile',
'laughing',
'wink',
'blush',
'yum',
'sunglasses',
'heart_eyes',
'kissing_heart',
'kissing',
'kissing_smiling_eyes',
'kissing_closed_eyes',
'relaxed',
'slightly_smiling_face',
'hugging_face',
'grinning_face_with_star_eyes',
'thinking_face',
'face_with_one_eyebrow_raised',
'neutral_face',
'expressionless',
'no_mouth',
'face_with_rolling_eyes',
'smirk',
'persevere',
'disappointed_relieved',
'open_mouth',
'zipper_mouth_face',
]}
/>
</div>
</util.ConversationContext>
```

View File

@ -0,0 +1,106 @@
import * as React from 'react';
import classNames from 'classnames';
import { noop } from 'lodash';
import { Manager, Popper, Reference } from 'react-popper';
import { createPortal } from 'react-dom';
import { EmojiPicker, Props as EmojiPickerProps } from './EmojiPicker';
import { LocalizerType } from '../../types/Util';
export type OwnProps = {
readonly i18n: LocalizerType;
};
export type Props = OwnProps &
Pick<
EmojiPickerProps,
'onPickEmoji' | 'skinTone' | 'onSetSkinTone' | 'recentEmojis'
>;
export const EmojiButton = React.memo(
({ i18n, onPickEmoji, skinTone, onSetSkinTone, recentEmojis }: Props) => {
const [open, setOpen] = React.useState(false);
const [popperRoot, setPopperRoot] = React.useState<HTMLElement | null>(
null
);
const handleClickButton = React.useCallback(
() => {
if (popperRoot) {
setOpen(false);
} else {
setOpen(true);
}
},
[popperRoot, setOpen]
);
const handleClose = React.useCallback(
() => {
setOpen(false);
},
[setOpen]
);
// Create popper root and handle outside clicks
React.useEffect(
() => {
if (open) {
const root = document.createElement('div');
setPopperRoot(root);
document.body.appendChild(root);
const handleOutsideClick = ({ target }: MouseEvent) => {
if (!root.contains(target as Node)) {
setOpen(false);
}
};
document.addEventListener('click', handleOutsideClick);
return () => {
document.body.removeChild(root);
document.removeEventListener('click', handleOutsideClick);
setPopperRoot(null);
};
}
return noop;
},
[open, setOpen, setPopperRoot]
);
return (
<Manager>
<Reference>
{({ ref }) => (
<button
ref={ref}
onClick={handleClickButton}
className={classNames({
'module-emoji-button__button': true,
'module-emoji-button__button--active': open,
})}
/>
)}
</Reference>
{open && popperRoot
? createPortal(
<Popper placement="top-start">
{({ ref, style }) => (
<EmojiPicker
ref={ref}
i18n={i18n}
style={style}
onPickEmoji={onPickEmoji}
onClose={handleClose}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}
recentEmojis={recentEmojis}
/>
)}
</Popper>,
popperRoot
)
: null}
</Manager>
);
}
);

View File

@ -0,0 +1,60 @@
#### Default
```jsx
<util.ConversationContext theme={util.theme}>
<EmojiPicker
i18n={util.i18n}
onPickEmoji={e => console.log('onPickEmoji', e)}
onSetSkinTone={t => console.log('onSetSkinTone', t)}
onClose={() => console.log('onClose')}
recentEmojis={[
'grinning',
'grin',
'joy',
'rolling_on_the_floor_laughing',
'smiley',
'smile',
'sweat_smile',
'laughing',
'wink',
'blush',
'yum',
'sunglasses',
'heart_eyes',
'kissing_heart',
'kissing',
'kissing_smiling_eyes',
'kissing_closed_eyes',
'relaxed',
'slightly_smiling_face',
'hugging_face',
'grinning_face_with_star_eyes',
'thinking_face',
'face_with_one_eyebrow_raised',
'neutral_face',
'expressionless',
'no_mouth',
'face_with_rolling_eyes',
'smirk',
'persevere',
'disappointed_relieved',
'open_mouth',
'zipper_mouth_face',
]}
/>
</util.ConversationContext>
```
#### No Recents
```jsx
<util.ConversationContext theme={util.theme}>
<EmojiPicker
i18n={util.i18n}
onPickEmoji={e => console.log('onPickEmoji', e)}
onSetSkinTone={t => console.log('onSetSkinTone', t)}
onClose={() => console.log('onClose')}
recentEmojis={[]}
/>
</util.ConversationContext>
```

View File

@ -0,0 +1,364 @@
import * as React from 'react';
import classNames from 'classnames';
import {
AutoSizer,
Grid,
GridCellRenderer,
SectionRenderedParams,
} from 'react-virtualized';
import {
chunk,
debounce,
findLast,
flatMap,
initial,
last,
zipObject,
} from 'lodash';
import { Emoji } from './Emoji';
import { dataByCategory, search } from './lib';
import { LocalizerType } from '../../types/Util';
export type OwnProps = {
readonly i18n: LocalizerType;
readonly onPickEmoji: (o: { skinTone: number; shortName: string }) => unknown;
readonly skinTone: number;
readonly onSetSkinTone: (tone: number) => unknown;
readonly recentEmojis: Array<string>;
readonly onClose: () => unknown;
};
export type Props = OwnProps & Pick<React.HTMLProps<HTMLDivElement>, 'style'>;
function focusRef(el: HTMLElement | null) {
if (el) {
el.focus();
}
}
const COL_COUNT = 8;
const categories = [
'recents',
'emoji',
'animal',
'food',
'activity',
'travel',
'object',
'symbol',
'flag',
];
export const EmojiPicker = React.memo(
React.forwardRef<HTMLDivElement, Props>(
// tslint:disable-next-line max-func-body-length
(
{
i18n,
onPickEmoji,
skinTone = 0,
onSetSkinTone,
recentEmojis,
style,
onClose,
}: Props,
ref
) => {
// Per design: memoize the initial recent emojis so the grid only updates after re-opening the picker.
const firstRecent = React.useMemo(() => {
return recentEmojis;
}, []);
const [selectedCategory, setSelectedCategory] = React.useState(
categories[0]
);
const [searchMode, setSearchMode] = React.useState(false);
const [searchText, setSearchText] = React.useState('');
const [scrollToRow, setScrollToRow] = React.useState(0);
const [selectedTone, setSelectedTone] = React.useState(skinTone);
const handleToggleSearch = React.useCallback(
() => {
setSearchText('');
setSelectedCategory(categories[0]);
setSearchMode(m => !m);
},
[setSearchText, setSearchMode]
);
const debounceSearchChange = React.useMemo(
() =>
debounce(query => {
setSearchText(query);
setScrollToRow(0);
}, 200),
[setSearchText, setScrollToRow]
);
const handleSearchChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
debounceSearchChange(e.currentTarget.value);
},
[debounceSearchChange]
);
const handlePickTone = React.useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
const { tone = '0' } = e.currentTarget.dataset;
const parsedTone = parseInt(tone, 10);
setSelectedTone(parsedTone);
onSetSkinTone(parsedTone);
},
[]
);
const handlePickEmoji = React.useCallback(
(e: React.MouseEvent<HTMLButtonElement>) => {
const { shortName } = e.currentTarget.dataset;
if (shortName) {
onPickEmoji({ skinTone: selectedTone, shortName });
}
},
[onClose, onPickEmoji, selectedTone]
);
// Handle escape key
React.useEffect(
() => {
const handler = (e: KeyboardEvent) => {
if (searchMode && e.key === 'Escape') {
setSearchText('');
setSearchMode(false);
setScrollToRow(0);
} else if (!searchMode) {
onClose();
}
};
document.addEventListener('keyup', handler);
return () => {
document.removeEventListener('keyup', handler);
};
},
[onClose, searchMode]
);
const emojiGrid = React.useMemo(
() => {
if (searchText) {
return chunk(search(searchText).map(e => e.short_name), COL_COUNT);
}
const [, ...cats] = categories;
const chunks = flatMap(cats, cat =>
chunk(dataByCategory[cat].map(e => e.short_name), COL_COUNT)
);
return [...chunk(firstRecent, COL_COUNT), ...chunks];
},
[dataByCategory, categories, firstRecent, searchText]
);
const catRowEnds = React.useMemo(
() => {
const rowEnds: Array<number> = [
Math.ceil(firstRecent.length / COL_COUNT) - 1,
];
const [, ...cats] = categories;
cats.forEach(cat => {
rowEnds.push(
Math.ceil(dataByCategory[cat].length / COL_COUNT) +
(last(rowEnds) as number)
);
});
return rowEnds;
},
[categories, dataByCategory]
);
const catToRowOffsets = React.useMemo(
() => {
const offsets = initial(catRowEnds).map(i => i + 1);
return zipObject(categories, [0, ...offsets]);
},
[categories, catRowEnds]
);
const catOffsetEntries = React.useMemo(
() => Object.entries(catToRowOffsets),
[catToRowOffsets]
);
const handleSelectCategory = React.useCallback(
({ currentTarget }: React.MouseEvent<HTMLButtonElement>) => {
const { category } = currentTarget.dataset;
if (category) {
setSelectedCategory(category);
setScrollToRow(catToRowOffsets[category]);
}
},
[catToRowOffsets, setSelectedCategory, setScrollToRow]
);
const cellRenderer = React.useCallback<GridCellRenderer>(
({ key, style: cellStyle, rowIndex, columnIndex }) => {
const shortName = emojiGrid[rowIndex][columnIndex];
return shortName ? (
<div
key={key}
className="module-emoji-picker__body__emoji-cell"
style={cellStyle}
>
<button
className="module-emoji-picker__button"
onClick={handlePickEmoji}
data-short-name={shortName}
title={shortName}
>
<Emoji shortName={shortName} skinTone={selectedTone} />
</button>
</div>
) : null;
},
[emojiGrid, selectedTone]
);
const getRowHeight = React.useCallback(
({ index }: { index: number }) => {
if (searchText) {
return 34;
}
if (catRowEnds.includes(index) && index !== last(catRowEnds)) {
return 44;
}
return 34;
},
[catRowEnds, searchText]
);
const onSectionRendered = React.useMemo(
() =>
debounce(({ rowStartIndex }: SectionRenderedParams) => {
const [cat] =
findLast(catOffsetEntries, ([, row]) => rowStartIndex >= row) ||
categories;
setSelectedCategory(cat);
}, 10),
[catOffsetEntries, categories]
);
return (
<div className="module-emoji-picker" ref={ref} style={style}>
<header className="module-emoji-picker__header">
<button
onClick={handleToggleSearch}
title={i18n('EmojiPicker--search-placeholder')}
className={classNames(
'module-emoji-picker__button',
'module-emoji-picker__button--icon',
searchMode
? 'module-emoji-picker__button--icon--close'
: 'module-emoji-picker__button--icon--search'
)}
/>
{searchMode ? (
<div className="module-emoji-picker__header__search-field">
<input
ref={focusRef}
className="module-emoji-picker__header__search-field__input"
placeholder={i18n('EmojiPicker--search-placeholder')}
onChange={handleSearchChange}
/>
</div>
) : (
categories.map(
cat =>
cat === 'recents' && firstRecent.length === 0 ? null : (
<button
key={cat}
data-category={cat}
title={cat}
onClick={handleSelectCategory}
className={classNames(
'module-emoji-picker__button',
'module-emoji-picker__button--icon',
`module-emoji-picker__button--icon--${cat}`,
selectedCategory === cat
? 'module-emoji-picker__button--selected'
: null
)}
/>
)
)
)}
</header>
{emojiGrid.length > 0 ? (
<div>
<AutoSizer>
{({ width, height }) => (
<Grid
key={searchText}
className="module-emoji-picker__body"
width={width}
height={height}
columnCount={COL_COUNT}
columnWidth={38}
rowHeight={getRowHeight}
rowCount={emojiGrid.length}
cellRenderer={cellRenderer}
scrollToRow={scrollToRow}
scrollToAlignment="start"
onSectionRendered={onSectionRendered}
/>
)}
</AutoSizer>
</div>
) : (
<div
className={classNames(
'module-emoji-picker__body',
'module-emoji-picker__body--empty'
)}
>
{i18n('EmojiPicker--empty')}
<Emoji
shortName="slightly_frowning_face"
size={16}
inline={true}
style={{ marginLeft: '4px' }}
/>
</div>
)}
<footer className="module-emoji-picker__footer">
{[0, 1, 2, 3, 4, 5].map(tone => (
<button
key={tone}
data-tone={tone}
onClick={handlePickTone}
title={i18n('EmojiPicker--skin-tone', [`${tone}`])}
className={classNames(
'module-emoji-picker__button',
'module-emoji-picker__button--footer',
selectedTone === tone
? 'module-emoji-picker__button--selected'
: null
)}
>
<Emoji shortName="hand" skinTone={tone} size={20} />
</button>
))}
</footer>
</div>
);
}
)
);

192
ts/components/emoji/lib.ts Normal file
View File

@ -0,0 +1,192 @@
// @ts-ignore: untyped json
import untypedData from 'emoji-datasource';
import {
compact,
flatMap,
groupBy,
isNumber,
keyBy,
map,
mapValues,
sortBy,
} from 'lodash';
import Fuse from 'fuse.js';
export type ValuesOf<T extends Array<any>> = T[number];
export const skinTones = ['1F3FB', '1F3FC', '1F3FD', '1F3FE', '1F3FF'];
export type SkinToneKey = '1F3FB' | '1F3FC' | '1F3FD' | '1F3FE' | '1F3FF';
export type EmojiData = {
name: string;
unified: string;
non_qualified: string | null;
docomo: string | null;
au: string | null;
softbank: string | null;
google: string | null;
image: string;
sheet_x: number;
sheet_y: number;
short_name: string;
short_names: Array<string>;
text: string | null;
texts: Array<string> | null;
category: string;
sort_order: number;
added_in: string;
has_img_apple: boolean;
has_img_google: boolean;
has_img_twitter: boolean;
has_img_emojione: boolean;
has_img_facebook: boolean;
has_img_messenger: boolean;
skin_variations?: {
[key: string]: {
unified: string;
non_qualified: null;
image: string;
sheet_x: number;
sheet_y: number;
added_in: string;
has_img_apple: boolean;
has_img_google: boolean;
has_img_twitter: boolean;
has_img_emojione: boolean;
has_img_facebook: boolean;
has_img_messenger: boolean;
};
};
};
const data: Array<EmojiData> = untypedData;
export const dataByShortName = keyBy(data, 'short_name');
data.forEach(emoji => {
const { short_names } = emoji;
if (short_names) {
short_names.forEach(name => {
dataByShortName[name] = emoji;
});
}
});
export const dataByCategory = mapValues(
groupBy(data, ({ category }) => {
if (category === 'Activities') {
return 'activity';
}
if (category === 'Animals & Nature') {
return 'animal';
}
if (category === 'Flags') {
return 'flag';
}
if (category === 'Food & Drink') {
return 'food';
}
if (category === 'Objects') {
return 'object';
}
if (category === 'Travel & Places') {
return 'travel';
}
if (category === 'Smileys & People') {
return 'emoji';
}
if (category === 'Symbols') {
return 'symbol';
}
return 'misc';
}),
arr => sortBy(arr, 'sort_order')
);
export function getSheetCoordinates(
shortName: keyof typeof dataByShortName,
skinTone?: SkinToneKey | number
): [number, number] {
const base = dataByShortName[shortName];
if (skinTone && base.skin_variations) {
const variation = isNumber(skinTone) ? skinTones[skinTone - 1] : skinTone;
const { sheet_x, sheet_y } = base.skin_variations[variation];
return [sheet_x, sheet_y];
}
return [base.sheet_x, base.sheet_y];
}
const fuse = new Fuse(data, {
shouldSort: true,
threshold: 0.3,
location: 0,
distance: 5,
maxPatternLength: 20,
minMatchCharLength: 1,
keys: ['name', 'short_name', 'short_names'],
});
export function search(query: string) {
return fuse.search(query);
}
const shortNames = new Set([
...map(data, 'short_name'),
...compact<string>(flatMap(data, 'short_names')),
]);
export function isShortName(name: string) {
return shortNames.has(name);
}
export function unifiedToEmoji(unified: string) {
return unified
.split('-')
.map(c => String.fromCodePoint(parseInt(c, 16)))
.join('');
}
export function convertShortName(shortName: string, skinTone: number = 0) {
const base = dataByShortName[shortName];
if (!base) {
return '';
}
if (skinTone && base.skin_variations) {
const toneKey = skinTones[0];
const variation = base.skin_variations[toneKey];
if (variation) {
return unifiedToEmoji(variation.unified);
}
}
return unifiedToEmoji(base.unified);
}
export function replaceColons(str: string) {
return str.replace(/:[a-z0-9-_+]+:(?::skin-tone-[1-4]:)?/gi, m => {
const [shortName = '', skinTone = '0'] = m
.replace('skin-tone-', '')
.split(':')
.filter(Boolean);
if (shortName) {
return convertShortName(shortName, parseInt(skinTone, 10));
}
return m;
});
}

View File

@ -1,4 +1,5 @@
import { actions as conversations } from './ducks/conversations';
import { actions as emojis } from './ducks/emojis';
import { actions as items } from './ducks/items';
import { actions as search } from './ducks/search';
import { actions as stickers } from './ducks/stickers';
@ -6,6 +7,7 @@ import { actions as user } from './ducks/user';
export const mapDispatchToProps = {
...conversations,
...emojis,
...items,
...search,
...stickers,

65
ts/state/ducks/emojis.ts Normal file
View File

@ -0,0 +1,65 @@
import { take, uniq } from 'lodash';
import { updateEmojiUsage } from '../../../js/modules/data';
// State
export type EmojisStateType = {
readonly recents: Array<string>;
};
// Actions
type UseEmojiPayloadType = string;
type UseEmojiAction = {
type: 'emojis/USE_EMOJI';
payload: Promise<UseEmojiPayloadType>;
};
type UseEmojiFulfilledAction = {
type: 'emojis/USE_EMOJI_FULFILLED';
payload: UseEmojiPayloadType;
};
export type EmojisActionType = UseEmojiAction | UseEmojiFulfilledAction;
// Action Creators
export const actions = {
useEmoji,
};
function useEmoji(shortName: string): UseEmojiAction {
return {
type: 'emojis/USE_EMOJI',
payload: doUseEmoji(shortName),
};
}
async function doUseEmoji(shortName: string): Promise<UseEmojiPayloadType> {
await updateEmojiUsage(shortName);
return shortName;
}
// Reducer
function getEmptyState(): EmojisStateType {
return {
recents: [],
};
}
export function reducer(
state: EmojisStateType = getEmptyState(),
action: EmojisActionType
): EmojisStateType {
if (action.type === 'emojis/USE_EMOJI_FULFILLED') {
const { payload } = action;
return {
...state,
recents: take(uniq([payload, ...state.recents]), 32),
};
}
return state;
}

View File

@ -5,6 +5,11 @@ import {
ConversationsStateType,
reducer as conversations,
} from './ducks/conversations';
import {
EmojisActionType,
EmojisStateType,
reducer as emojis,
} from './ducks/emojis';
import {
ItemsActionType,
ItemsStateType,
@ -24,6 +29,7 @@ import { reducer as user, UserStateType } from './ducks/user';
export type StateType = {
conversations: ConversationsStateType;
emojis: EmojisStateType;
items: ItemsStateType;
search: SearchStateType;
stickers: StickersStateType;
@ -31,13 +37,15 @@ export type StateType = {
};
export type ActionsType =
| ItemsActionType
| EmojisActionType
| ConversationActionType
| ItemsActionType
| StickersActionType
| SearchActionType;
export const reducers = {
conversations,
emojis,
items,
search,
stickers,

View File

@ -0,0 +1,16 @@
import React from 'react';
import { Provider } from 'react-redux';
import { Store } from 'redux';
import { SmartEmojiButton } from '../smart/EmojiButton';
// Workaround: A react component's required properties are filtering up through connect()
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31363
const FilteredEmojiButton = SmartEmojiButton as any;
export const createEmojiButton = (store: Store, props: Object) => (
<Provider store={store}>
<FilteredEmojiButton {...props} />
</Provider>
);

View File

@ -0,0 +1,54 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { get } from 'lodash';
import { mapDispatchToProps } from '../actions';
import { EmojiButton, Props } from '../../components/emoji/EmojiButton';
import { StateType } from '../reducer';
import { getIntl } from '../selectors/user';
const mapStateToProps = (state: StateType) => {
const { recents } = state.emojis;
return {
i18n: getIntl(state),
recentEmojis: recents,
skinTone: get(state, ['items', 'skinTone', 'value'], 0),
};
};
const dispatchPropsMap = {
...mapDispatchToProps,
onSetSkinTone: (tone: number) => mapDispatchToProps.putItem('skinTone', tone),
};
type OnPickEmojiType = Props['onPickEmoji'];
type UseEmojiType = typeof mapDispatchToProps.useEmoji;
export type OwnProps = {
onPickEmoji: OnPickEmojiType;
};
const selectOnPickEmoji = createSelector(
(onPickEmoji: OnPickEmojiType) => onPickEmoji,
(_onPickEmoji: OnPickEmojiType, useEmoji: UseEmojiType) => useEmoji,
(onPickEmoji, useEmoji): OnPickEmojiType => e => {
onPickEmoji(e);
useEmoji(e.shortName);
}
);
const mergeProps = (
stateProps: ReturnType<typeof mapStateToProps>,
dispatchProps: typeof dispatchPropsMap,
ownProps: OwnProps
) => ({
...ownProps,
...stateProps,
...dispatchProps,
onPickEmoji: selectOnPickEmoji(ownProps.onPickEmoji, dispatchProps.useEmoji),
});
const smart = connect(mapStateToProps, dispatchPropsMap, mergeProps);
export const SmartEmojiButton = smart(EmojiButton);

View File

@ -24,18 +24,6 @@ export function findImage(value: string, variation?: string) {
return instance.find_image(value, variation);
}
export function replaceColons(str: string) {
return str.replace(instance.rx_colons, m => {
const name = m.substr(1, m.length - 2).toLowerCase();
const code = instance.map.colons[name];
if (code) {
return instance.data[code][0][0];
}
return m;
});
}
function getCountOfAllMatches(str: string, regex: RegExp) {
let match = regex.exec(str);
let count = 0;

View File

@ -2985,108 +2985,6 @@
"updated": "2018-09-18T19:19:27.699Z",
"reasonDetail": "It's setting the html of the element to the previous HTML, just with the emoji replaced"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/emoji-panel/dist/emoji-panel.js",
"line": "\t el.innerHTML = '';",
"lineNumber": 94,
"reasonCategory": "usageTrusted",
"updated": "2018-09-15T00:38:04.183Z",
"reasonDetail": "Hard-coded value"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/emoji-panel/dist/emoji-panel.js",
"line": "\t panelEl.innerHTML = _template2.default;",
"lineNumber": 154,
"reasonCategory": "usageTrusted",
"updated": "2018-09-18T19:19:27.699Z",
"reasonDetail": "In this file, _template2.default is a hardcoded string generated from emoji data"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/emoji-panel/dist/example.js",
"line": "\t codeEl.innerHTML = codeEl.innerHTML.replace(/dist\\/emoji-panel-.*-.*.min.css/g, newHref);",
"lineNumber": 67,
"reasonCategory": "exampleCode",
"updated": "2018-09-15T00:38:04.183Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/emoji-panel/dist/example.js",
"line": "\t$('#example-4-btn').click(function (e) {",
"lineNumber": 101,
"reasonCategory": "exampleCode",
"updated": "2018-09-19T21:59:32.770Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/emoji-panel/dist/example.js",
"line": "\t $('#example-4').dialog({",
"lineNumber": 102,
"reasonCategory": "exampleCode",
"updated": "2018-09-19T21:59:32.770Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/emoji-panel/lib/emoji-panel.js",
"line": "\t el.innerHTML = '';",
"lineNumber": 103,
"reasonCategory": "usageTrusted",
"updated": "2018-09-15T00:38:04.183Z",
"reasonDetail": "Hard-coded value"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/emoji-panel/lib/emoji-panel.js",
"line": "\t panelEl.innerHTML = _template2.default;",
"lineNumber": 163,
"reasonCategory": "usageTrusted",
"updated": "2018-09-18T19:19:27.699Z",
"reasonDetail": "In this file, _template2.default is a hardcoded string generated from emoji data"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/emoji-panel/lib/example.js",
"line": "\t codeEl.innerHTML = codeEl.innerHTML.replace(/dist\\/emoji-panel-.*-.*.min.css/g, newHref);",
"lineNumber": 76,
"reasonCategory": "exampleCode",
"updated": "2018-09-18T19:19:27.699Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/emoji-panel/lib/example.js",
"line": "\t$('#example-4-btn').click(function (e) {",
"lineNumber": 110,
"reasonCategory": "exampleCode",
"updated": "2018-09-19T21:59:32.770Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/emoji-panel/lib/example.js",
"line": "\t $('#example-4').dialog({",
"lineNumber": 111,
"reasonCategory": "exampleCode",
"updated": "2018-09-19T21:59:32.770Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/emoji-panel/src/create-panel.js",
"line": " panelEl.innerHTML = panelTemplate;",
"lineNumber": 7,
"reasonCategory": "usageTrusted",
"updated": "2018-09-18T19:19:27.699Z",
"reasonDetail": "In this file, panelTemplate is a hardcoded string generated from emoji data"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/emoji-panel/src/emoji-panel.js",
"line": " el.innerHTML = '';",
"lineNumber": 25,
"reasonCategory": "usageTrusted",
"updated": "2018-09-15T00:38:04.183Z",
"reasonDetail": "Hard-coded value"
},
{
"rule": "thenify-multiArgs",
"path": "node_modules/es6-promisify/dist/promisify.js",
@ -6168,5 +6066,13 @@
"lineNumber": 60,
"reasonCategory": "falseMatch",
"updated": "2019-05-02T20:44:56.470Z"
},
{
"rule": "jQuery-load(",
"path": "js/modules/emojis.js",
"line": "async function load() {",
"lineNumber": 13,
"reasonCategory": "falseMatch",
"updated": "2019-05-23T22:27:53.554Z"
}
]
]

View File

@ -2772,12 +2772,6 @@ emoji-js@3.4.0:
dependencies:
emoji-datasource "4.0.0"
"emoji-panel@https://github.com/scottnonnenberg-signal/emoji-panel.git#v0.5.5":
version "0.5.5"
resolved "https://github.com/scottnonnenberg-signal/emoji-panel.git#81e236e03458a44d4a174ab5f367cb4b9b1b2f97"
dependencies:
emoji-datasource "4.0.0"
"emoji-regex@>=6.0.0 <=6.1.1":
version "6.1.1"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e"
@ -3732,6 +3726,11 @@ functional-red-black-tree@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
fuse.js@^3.4.4:
version "3.4.4"
resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.4.4.tgz#f98f55fcb3b595cf6a3e629c5ffaf10982103e95"
integrity sha512-pyLQo/1oR5Ywf+a/tY8z4JygnIglmRxVUOiyFAbd11o9keUDpUJSMGRWJngcnkURj30kDHPmhoKY8ChJiz3EpQ==
gauge@~2.7.1, gauge@~2.7.3:
version "2.7.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7"