Fuzzy-Searchable Emoji Picker
|
@ -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."
|
||||
|
|
64
app/sql.js
|
@ -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;
|
||||
|
|
|
@ -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'>
|
||||
|
|
11
images/emoji-activity-filled-20.svg
Executable 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 |
11
images/emoji-activity-outline-20.svg
Executable 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: 1,013 B |
14
images/emoji-animal-filled-20.svg
Normal 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 |
17
images/emoji-animal-outline-20.svg
Normal 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
|
@ -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 |
8
images/emoji-flag-filled-20.svg
Normal 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 |
8
images/emoji-flag-outline-20.svg
Normal 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
|
@ -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 |
15
images/emoji-food-outline-20.svg
Executable 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 |
7
images/emoji-object-filled-20.svg
Executable 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 |
8
images/emoji-object-outline-20.svg
Executable 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
|
@ -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 |
8
images/emoji-symbol-filled-20.svg
Executable 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 |
10
images/emoji-symbol-outline-20.svg
Executable 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 |
8
images/emoji-travel-filled-20.svg
Normal 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 |
10
images/emoji-travel-outline-20.svg
Normal 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 |
7
images/recent-outline-20.svg
Normal 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 |
|
@ -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
|
||||
|
|
5
js/modules/data.d.ts
vendored
|
@ -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 }>>;
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
18
package.json
|
@ -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": [{
|
||||
"extraFiles": [
|
||||
{
|
||||
"from": "node_modules/@journeyapps/sqlcipher/build/Release/",
|
||||
"to": ".",
|
||||
"filter": ["msvcp140.dll", "vcruntime140.dll"]
|
||||
}]
|
||||
"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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -3173,30 +3173,42 @@
|
|||
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;
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
21
ts/components/emoji/Emoji.md
Normal 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>
|
||||
```
|
43
ts/components/emoji/Emoji.tsx
Normal 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`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
56
ts/components/emoji/EmojiButton.md
Normal 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>
|
||||
```
|
106
ts/components/emoji/EmojiButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
);
|
60
ts/components/emoji/EmojiPicker.md
Normal 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>
|
||||
```
|
364
ts/components/emoji/EmojiPicker.tsx
Normal 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
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
|
|
16
ts/state/roots/createEmojiButton.tsx
Normal 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>
|
||||
);
|
54
ts/state/smart/EmojiButton.tsx
Normal 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);
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
]
|
11
yarn.lock
|
@ -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"
|
||||
|
|