New composition area with emoji typeahead

This commit is contained in:
Ken Powers 2019-06-27 16:35:21 -04:00 committed by Scott Nonnenberg
parent e62a1a7812
commit 7b645011c2
95 changed files with 1740 additions and 1293 deletions

View File

@ -947,16 +947,6 @@
"message": "File icon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "سيجنال للحاسوب. مرحبا بك ",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "File icon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Добре дошли в Сигнал за настолен компютър",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Icona del fitxer",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Imatge emoji de «$title$»",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Us donem la benvinguda al Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "File icon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Vítejte v aplikaci Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Filikon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji billede af '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Velkommen til Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Dateisymbol",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emojibild von »$title$«",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Willkommen bei Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Εικονίδιο αρχείου",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Εικόνα emoji '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Καλώς ορίσατε στο Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -1067,16 +1067,6 @@
"description":
"Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Welcome to Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Dosierpiktogramo",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoĝibildo de „$title$“",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Bonvenon al Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Icono de archivo",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji representando '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Bienvenida a Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -811,16 +811,6 @@
"message": "File icon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Bienvenido a Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Faili ikoon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "$title$ emoji-pilt",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Tere tulemast Signal Desktopi kasutama",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "آیکون فایل",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "تصویر ایموجی '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "به Signal Desktop خوش‌آمدید",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Tiedoston ikoni",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji kuva: '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Tervetuloa Signal Desktopiin",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Icône de fichier",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Image émoji de « $title$ »",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Bienvenue sur Signal Desktop pour ordinateur",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "צלמית קובץ",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "תמונת אימוג'י של '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "ברוך הבא אל Signal Desktop עבודה",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "File icon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Signal डेस्कटॉप में आपका स्वागत है",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "File icon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Dobrodošli u Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Fájl ikon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "'$title$'-t ábrázoló emoji",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Üdvözöl a Signal Desktop!",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Ikon Berkas",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Gambar emoji dari '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Selamat datang di Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Icona del file",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji immagine di '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Benvenuto in Signal per Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "ファイルのアイコン",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "「$title$」の絵文字画像",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Signal Desktopにようこそ",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "រូបតំណាងឯកសារ",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "រូបEmoji របស់ '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "ស្វាគមន៍មកកាន់ Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "File icon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Signal ಗಣಕತೆರೆಗೆ ಸ್ವಾಗತ",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "File icon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Welcome to Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Failo piktograma",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Šypsenėlė \"$title$\"",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Sveiki atvykę į Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "File icon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Welcome to Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Filikon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji-bilde av «$title$»",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Velkommen til Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Bestandspictogram",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji-afbeelding $title$",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Welkom bij Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Filikon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji-bilde av «$title$»",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Velkommen til Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Filikon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji-bilde av «$title$»",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Velkommen til Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Ikona pliku",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Ikonka emoji '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Witamy w Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Ícone do arquivo",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Boas-vindas ao Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Ícone do ficheiro",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji de '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "O Signal Desktop dá-lhe as boas-vindas!",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Icoană fișier",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Imagine emoji pentru '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Bun venit la Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Значок файла",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Добро пожаловать в Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Ikona súboru",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji \"$title$\"",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Vitajte v aplikácii Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Ikona datoteke",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji znak za '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Dobrodošli v aplikaciji Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Ikonë kartele",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Figurë emoji e '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Mirë se vini te Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "File icon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Добродошли у Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Filikon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emojibild av '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Välkommen till Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "ไอคอนไฟล์",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "รูปอีโมจิของ '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "ยินดีต้อนรับสู่ Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "Dosya ikonu",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "'$title$' emoji resmi",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Signal Desktop'a Hoş Geldiniz",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "File icon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Ласкаво просимо до Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "File icon",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "Emoji image of '$title$'",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "Chào mừng đến với Signal Desktop",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "文件图标",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": "“$title$”的表情图片",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "欢迎来到Signal桌面版",
"description": "Welcome title on the install page"

View File

@ -947,16 +947,6 @@
"message": "檔案圖示",
"description": "Used in the media gallery documents tab to visually represent a file"
},
"emojiAlt": {
"message": " '$title$' 的表情符號圖片",
"description": "Used in the alt tag of all emoji images",
"placeholders": {
"title": {
"content": "$1",
"example": "grinning"
}
}
},
"installWelcome": {
"message": "歡迎來到 Signal Desktop版",
"description": "Welcome title on the install page"

View File

@ -22,6 +22,7 @@
<title>Signal</title>
<link href='images/icon_128.png' rel='shortcut icon'>
<link href="stylesheets/manifest.css" rel="stylesheet" type="text/css" />
<link href="node_modules/draft-js/dist/Draft.css" rel="stylesheet" type="text/css" />
<!--
When making changes to these templates, be sure to update test/index.html as well
@ -116,9 +117,7 @@
<div class='compose'>
<form class='send clearfix file-input'>
<div class='flex'>
<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='composition-area-placeholder'></div>
<div class='capture-audio'>
<button class='microphone'></button>
</div>

View File

@ -5,7 +5,6 @@
"license": "GPLV3",
"private": true,
"dependencies": {
"autosize": "~4.0.0",
"indexeddb-backbonejs-adapter": "*",
"mp3lameencoder": "https://github.com/higuma/mp3-lame-encoder-js.git",
"protobuf": "~3.8.0",
@ -16,9 +15,6 @@
"mock-socket": "~0.3.2"
},
"preen": {
"autosize": [
"dist/autosize.js"
],
"bytebuffer": [
"dist/ByteBufferAB.js"
],

View File

@ -1,292 +0,0 @@
/*!
Autosize 4.0.0
license: MIT
http://www.jacklmoore.com/autosize
*/
(function (global, factory) {
if (typeof define === 'function' && define.amd) {
define(['exports', 'module'], factory);
} else if (typeof exports !== 'undefined' && typeof module !== 'undefined') {
factory(exports, module);
} else {
var mod = {
exports: {}
};
factory(mod.exports, mod);
global.autosize = mod.exports;
}
})(this, function (exports, module) {
'use strict';
var map = typeof Map === "function" ? new Map() : (function () {
var keys = [];
var values = [];
return {
has: function has(key) {
return keys.indexOf(key) > -1;
},
get: function get(key) {
return values[keys.indexOf(key)];
},
set: function set(key, value) {
if (keys.indexOf(key) === -1) {
keys.push(key);
values.push(value);
}
},
'delete': function _delete(key) {
var index = keys.indexOf(key);
if (index > -1) {
keys.splice(index, 1);
values.splice(index, 1);
}
}
};
})();
var createEvent = function createEvent(name) {
return new Event(name, { bubbles: true });
};
try {
new Event('test');
} catch (e) {
// IE does not support `new Event()`
createEvent = function (name) {
var evt = document.createEvent('Event');
evt.initEvent(name, true, false);
return evt;
};
}
function assign(ta) {
if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || map.has(ta)) return;
var heightOffset = null;
var clientWidth = ta.clientWidth;
var cachedHeight = null;
function init() {
var style = window.getComputedStyle(ta, null);
if (style.resize === 'vertical') {
ta.style.resize = 'none';
} else if (style.resize === 'both') {
ta.style.resize = 'horizontal';
}
if (style.boxSizing === 'content-box') {
heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom));
} else {
heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
}
// Fix when a textarea is not on document body and heightOffset is Not a Number
if (isNaN(heightOffset)) {
heightOffset = 0;
}
update();
}
function changeOverflow(value) {
{
// Chrome/Safari-specific fix:
// When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
// made available by removing the scrollbar. The following forces the necessary text reflow.
var width = ta.style.width;
ta.style.width = '0px';
// Force reflow:
/* jshint ignore:start */
ta.offsetWidth;
/* jshint ignore:end */
ta.style.width = width;
}
ta.style.overflowY = value;
}
function getParentOverflows(el) {
var arr = [];
while (el && el.parentNode && el.parentNode instanceof Element) {
if (el.parentNode.scrollTop) {
arr.push({
node: el.parentNode,
scrollTop: el.parentNode.scrollTop
});
}
el = el.parentNode;
}
return arr;
}
function resize() {
var originalHeight = ta.style.height;
var overflows = getParentOverflows(ta);
var docTop = document.documentElement && document.documentElement.scrollTop; // Needed for Mobile IE (ticket #240)
ta.style.height = '';
var endHeight = ta.scrollHeight + heightOffset;
if (ta.scrollHeight === 0) {
// If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
ta.style.height = originalHeight;
return;
}
ta.style.height = endHeight + 'px';
// used to check if an update is actually necessary on window.resize
clientWidth = ta.clientWidth;
// prevents scroll-position jumping
overflows.forEach(function (el) {
el.node.scrollTop = el.scrollTop;
});
if (docTop) {
document.documentElement.scrollTop = docTop;
}
}
function update() {
resize();
var styleHeight = Math.round(parseFloat(ta.style.height));
var computed = window.getComputedStyle(ta, null);
// Using offsetHeight as a replacement for computed.height in IE, because IE does not account use of border-box
var actualHeight = computed.boxSizing === 'content-box' ? Math.round(parseFloat(computed.height)) : ta.offsetHeight;
// The actual height not matching the style height (set via the resize method) indicates that
// the max-height has been exceeded, in which case the overflow should be allowed.
if (actualHeight !== styleHeight) {
if (computed.overflowY === 'hidden') {
changeOverflow('scroll');
resize();
actualHeight = computed.boxSizing === 'content-box' ? Math.round(parseFloat(window.getComputedStyle(ta, null).height)) : ta.offsetHeight;
}
} else {
// Normally keep overflow set to hidden, to avoid flash of scrollbar as the textarea expands.
if (computed.overflowY !== 'hidden') {
changeOverflow('hidden');
resize();
actualHeight = computed.boxSizing === 'content-box' ? Math.round(parseFloat(window.getComputedStyle(ta, null).height)) : ta.offsetHeight;
}
}
if (cachedHeight !== actualHeight) {
cachedHeight = actualHeight;
var evt = createEvent('autosize:resized');
try {
ta.dispatchEvent(evt);
} catch (err) {
// Firefox will throw an error on dispatchEvent for a detached element
// https://bugzilla.mozilla.org/show_bug.cgi?id=889376
}
}
}
var pageResize = function pageResize() {
if (ta.clientWidth !== clientWidth) {
update();
}
};
var destroy = (function (style) {
window.removeEventListener('resize', pageResize, false);
ta.removeEventListener('input', update, false);
ta.removeEventListener('keyup', update, false);
ta.removeEventListener('autosize:destroy', destroy, false);
ta.removeEventListener('autosize:update', update, false);
Object.keys(style).forEach(function (key) {
ta.style[key] = style[key];
});
map['delete'](ta);
}).bind(ta, {
height: ta.style.height,
resize: ta.style.resize,
overflowY: ta.style.overflowY,
overflowX: ta.style.overflowX,
wordWrap: ta.style.wordWrap
});
ta.addEventListener('autosize:destroy', destroy, false);
// IE9 does not fire onpropertychange or oninput for deletions,
// so binding to onkeyup to catch most of those events.
// There is no way that I know of to detect something like 'cut' in IE9.
if ('onpropertychange' in ta && 'oninput' in ta) {
ta.addEventListener('keyup', update, false);
}
window.addEventListener('resize', pageResize, false);
ta.addEventListener('input', update, false);
ta.addEventListener('autosize:update', update, false);
ta.style.overflowX = 'hidden';
ta.style.wordWrap = 'break-word';
map.set(ta, {
destroy: destroy,
update: update
});
init();
}
function destroy(ta) {
var methods = map.get(ta);
if (methods) {
methods.destroy();
}
}
function update(ta) {
var methods = map.get(ta);
if (methods) {
methods.update();
}
}
var autosize = null;
// Do nothing in Node.js environment and IE8 (or lower)
if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
autosize = function (el) {
return el;
};
autosize.destroy = function (el) {
return el;
};
autosize.update = function (el) {
return el;
};
} else {
autosize = function (el, options) {
if (el) {
Array.prototype.forEach.call(el.length ? el : [el], function (x) {
return assign(x, options);
});
}
return el;
};
autosize.destroy = function (el) {
if (el) {
Array.prototype.forEach.call(el.length ? el : [el], destroy);
}
return el;
};
autosize.update = function (el) {
if (el) {
Array.prototype.forEach.call(el.length ? el : [el], update);
}
return el;
};
}
module.exports = autosize;
});

8
images/emoji-object-filled-20.svg Executable file → Normal file
View File

@ -1,7 +1 @@
<?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>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>emoji-object-solid-20</title><path d="M10,1A6.487,6.487,0,0,0,3.5,7.5a6.773,6.773,0,0,0,1.4,4.1S7.49,13.987,7.49,16v1.5A1.5,1.5,0,0,0,8.99,19h2a1.5,1.5,0,0,0,1.5-1.5V16h.01c0-2,2.5-4.4,2.5-4.4a6.1,6.1,0,0,0,1.5-4.1A6.487,6.487,0,0,0,10,1Zm1,16.5H9v-2h2Z"/></svg>

Before

Width:  |  Height:  |  Size: 591 B

After

Width:  |  Height:  |  Size: 352 B

9
images/emoji-object-outline-20.svg Executable file → Normal file
View File

@ -1,8 +1 @@
<?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>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>emoji-object-outline-20</title><path d="M14.1,2.5A6.291,6.291,0,0,0,10,1,6.487,6.487,0,0,0,3.5,7.5a6.773,6.773,0,0,0,1.4,4.1c1.3,1.7,2.6,2.9,2.6,4.4v1.5A1.5,1.5,0,0,0,9,19h2a1.5,1.5,0,0,0,1.5-1.5V16c0-1.5,1.2-2.8,2.6-4.4A6.4,6.4,0,0,0,14.1,2.5Zm-3.09,15h-2V16h2Zm2.89-6.8a25.578,25.578,0,0,0-2.6,3.8H8.7a28.167,28.167,0,0,0-2.6-3.9A4.887,4.887,0,0,1,5,7.5a5,5,0,0,1,10,0A5.167,5.167,0,0,1,13.9,10.7Z"/></svg>

Before

Width:  |  Height:  |  Size: 711 B

After

Width:  |  Height:  |  Size: 498 B

9
images/emoji-symbol-filled-20.svg Executable file → Normal file
View File

@ -1,8 +1 @@
<?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>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>emoji-symbol-solid-20</title><path d="M16,2H4A3,3,0,0,0,1,5V15a3,3,0,0,0,3,3H16a3,3,0,0,0,3-3V5A3,3,0,0,0,16,2Zm-.829,6.911H12.793l-.464,2.178h2.077V12.54h-2.38L11.5,15H10.011l.523-2.46H8.4L7.873,15H6.381l.524-2.46H4.829V11.089H7.207l.464-2.178H5.594V7.46h2.38L8.5,5H9.99L9.466,7.46H11.6L12.127,5h1.492L13.1,7.46h2.076Zm-6.008,0H11.3l-.464,2.178H8.7Z"/></svg>

Before

Width:  |  Height:  |  Size: 596 B

After

Width:  |  Height:  |  Size: 449 B

11
images/emoji-symbol-outline-20.svg Executable file → Normal file
View File

@ -1,10 +1 @@
<?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>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>emoji-symbol-outline-20</title><path d="M16,3.5A1.5,1.5,0,0,1,17.5,5V15A1.5,1.5,0,0,1,16,16.5H4A1.5,1.5,0,0,1,2.5,15V5A1.5,1.5,0,0,1,4,3.5H16M16,2H4A3,3,0,0,0,1,5V15a3,3,0,0,0,3,3H16a3,3,0,0,0,3-3V5a3,3,0,0,0-3-3ZM10.011,15l.523-2.46H8.4L7.873,15H6.381l.524-2.46H4.829V11.089H7.207l.464-2.178H5.594V7.46h2.38L8.5,5H9.99L9.466,7.46H11.6L12.127,5h1.492L13.1,7.46h2.076V8.911H12.793l-.464,2.178h2.077V12.54h-2.38L11.5,15ZM11.3,8.911H9.163L8.7,11.089h2.138Z"/></svg>

Before

Width:  |  Height:  |  Size: 877 B

After

Width:  |  Height:  |  Size: 552 B

View File

@ -74,11 +74,10 @@ const {
} = require('../../ts/components/conversation/VerificationNotification');
// State
const { createEmojiButton } = require('../../ts/state/roots/createEmojiButton');
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
const {
createStickerButton,
} = require('../../ts/state/roots/createStickerButton');
createCompositionArea,
} = require('../../ts/state/roots/createCompositionArea');
const { createLeftPane } = require('../../ts/state/roots/createLeftPane');
const {
createStickerManager,
} = require('../../ts/state/roots/createStickerManager');
@ -286,9 +285,8 @@ exports.setup = (options = {}) => {
};
const Roots = {
createEmojiButton,
createCompositionArea,
createLeftPane,
createStickerButton,
createStickerManager,
createStickerPreviewModal,
};

View File

@ -92,6 +92,7 @@
this.listenTo(this.model, 'change:verified', this.onVerifiedChange);
this.listenTo(this.model, 'newmessage', this.addMessage);
this.listenTo(this.model, 'opened', this.onOpened);
this.listenTo(this.model, 'backgrounded', this.resetEmojiResults);
this.listenTo(this.model, 'prune', this.onPrune);
this.listenTo(this.model, 'unload', () => this.unload('model trigger'));
this.listenTo(this.model, 'typing-update', this.renderTypingBubble);
@ -200,11 +201,6 @@
this.$('.discussion-container').append(this.view.el);
this.view.render();
this.$messageField = this.$('.send-message');
this.onResize = this.forceUpdateMessageFieldSize.bind(this);
this.window.addEventListener('resize', this.onResize);
this.onFocus = () => {
if (this.$el.css('display') !== 'none') {
this.markRead();
@ -222,36 +218,22 @@
this.$('.send-message').blur(this.unfocusBottomBar.bind(this));
this.setupHeader();
this.setupEmojiPickerButton();
this.setupStickerPickerButton();
this.lastSelectionStart = 0;
document.addEventListener(
'selectionchange',
this.updateLastSelectionStart.bind(this, undefined)
);
this.setupCompositionArea();
},
events: {
'submit .send': 'clickSend',
'input .send-message': 'updateMessageFieldSize',
'keydown .send-message': 'updateMessageFieldSize',
'keyup .send-message': 'onKeyUp',
click: 'onClick',
'click .emoji-button-placeholder': 'onClickPlaceholder',
'click .sticker-button-placeholder': 'onClickPlaceholder',
'click .composition-area-placeholder': 'onClickPlaceholder',
'click .bottom-bar': 'focusMessageField',
'click .capture-audio .microphone': 'captureAudio',
'click .module-scroll-down': 'scrollToBottom',
'focus .send-message': 'focusBottomBar',
'change .file-input': 'toggleMicrophone',
'blur .send-message': 'unfocusBottomBar',
'loadMore .message-list': 'loadMoreMessages',
'newOffscreenMessage .message-list': 'addScrollDownButtonWithCount',
'atBottom .message-list': 'removeScrollDownButton',
'farFromBottom .message-list': 'addScrollDownButton',
'lazyScroll .message-list': 'onLazyScroll',
'force-resize': 'forceUpdateMessageFieldSize',
'click button.paperclip': 'onChooseAttachment',
'change input.file-input': 'onChoseAttachment',
@ -331,52 +313,31 @@
this.$('.conversation-header').append(this.titleView.el);
},
setupEmojiPickerButton() {
const props = {
onForceSend: () => {
this.sendMessage({});
},
onPickEmoji: e => this.insertEmoji(e),
onClose: () => {
const textarea = this.$messageField[0];
textarea.focus();
const newPos = textarea.value.length;
textarea.selectionStart = newPos;
textarea.selectionEnd = newPos;
this.forceUpdateLastSelectionStart(newPos);
},
};
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;
}
setupCompositionArea() {
const compositionApi = { current: null };
this.compositionApi = compositionApi;
const props = {
compositionApi,
onClickAddPack: () => this.showStickerManager(),
onPickSticker: (packId, stickerId) =>
this.sendStickerMessage({ packId, stickerId }),
onSubmit: message => this.sendMessage(message),
onDirtyChange: dirty => this.toggleMicrophone(dirty),
onEditorStateChange: (msg, caretLocation) =>
this.onEditorStateChange(msg, caretLocation),
onEditorSizeChange: rect => this.onEditorSizeChange(rect),
};
this.stickerButtonView = new Whisper.ReactWrapperView({
className: 'sticker-button-wrapper',
JSX: Signal.State.Roots.createStickerButton(window.reduxStore, props),
this.compositionAreaView = new Whisper.ReactWrapperView({
className: 'composition-area-wrapper',
JSX: Signal.State.Roots.createCompositionArea(window.reduxStore, props),
});
// Finally, add it to the DOM
this.$('.sticker-button-placeholder').append(this.stickerButtonView.el);
this.$('.composition-area-placeholder').append(
this.compositionAreaView.el
);
},
// We need this, or clicking the reactified buttons will submit the form and send any
@ -479,14 +440,7 @@
}
}
this.window.removeEventListener('resize', this.onResize);
this.window.removeEventListener('focus', this.onFocus);
document.removeEventListener(
'selectionchange',
this.updateLastSelectionStart
);
window.autosize.destroy(this.$messageField);
this.view.remove();
@ -628,11 +582,8 @@
}
},
toggleMicrophone() {
if (
this.$('.send-message').val().length > 0 ||
this.fileInput.hasFiles()
) {
toggleMicrophone(dirty = false) {
if (dirty || this.fileInput.hasFiles()) {
this.$('.capture-audio').hide();
} else {
this.$('.capture-audio').show();
@ -664,7 +615,7 @@
view.on('closed', this.endCaptureAudio.bind(this));
view.$el.appendTo(this.$('.capture-audio'));
this.$('.send-message').attr('disabled', true);
this.disableMessageField();
this.$('.microphone').hide();
},
handleAudioCapture(blob) {
@ -673,10 +624,10 @@
file: blob,
isVoiceNote: true,
});
this.$('.bottom-bar form').submit();
this.sendMessage();
},
endCaptureAudio() {
this.$('.send-message').removeAttr('disabled');
this.enableMessageField();
this.$('.microphone').show();
this.captureAudioView = null;
},
@ -745,7 +696,6 @@
messagesLoaded.then(this.onLoaded.bind(this), this.onLoaded.bind(this));
this.view.resetScrollPosition();
this.$el.trigger('force-resize');
this.focusMessageField();
this.renderTypingBubble();
@ -1088,13 +1038,28 @@
return;
}
this.$messageField.focus();
const { compositionApi } = this;
if (compositionApi && compositionApi.current) {
compositionApi.current.focusInput();
}
},
focusMessageFieldAndClearDisabled() {
this.$messageField.removeAttr('disabled');
this.$messageField.focus();
this.updateLastSelectionStart();
this.compositionApi.current.setDisabled(false);
this.focusMessageField();
},
disableMessageField() {
this.compositionApi.current.setDisabled(true);
},
enableMessageField() {
this.compositionApi.current.setDisabled(false);
},
resetEmojiResults() {
this.compositionApi.current.resetEmojiResults(false);
},
async loadMoreMessages() {
@ -1648,7 +1613,6 @@
view.remove();
if (this.panels.length === 0) {
this.$el.trigger('force-resize');
// Make sure poppers are positioned properly
window.dispatchEvent(new Event('resize'));
}
@ -1716,36 +1680,6 @@
});
},
async clickSend(e, options) {
e.preventDefault();
this.sendStart = Date.now();
this.$messageField.attr('disabled', true);
try {
const contacts = await this.getUntrustedContacts(options);
if (contacts && contacts.length) {
const sendAnyway = await this.showSendAnywayDialog(contacts);
if (sendAnyway) {
this.clickSend(e, { force: true });
return;
}
this.focusMessageFieldAndClearDisabled();
return;
}
this.sendMessage(e);
} catch (error) {
this.focusMessageFieldAndClearDisabled();
window.log.error(
'clickSend error:',
error && error.stack ? error.stack : error
);
}
},
async sendStickerMessage(options = {}) {
try {
const contacts = await this.getUntrustedContacts(options);
@ -1799,34 +1733,6 @@
return null;
},
insertEmoji({ shortName, skinTone }) {
const skinReplacement = window.Signal.Emojis.hasVariation(
shortName,
skinTone
)
? `:skin-tone-${skinTone}:`
: '';
const colons = `:${shortName}:${skinReplacement}`;
const textarea = this.$messageField[0];
const hasFocus = document.activeElement === textarea;
const startPos = hasFocus
? textarea.selectionStart
: this.lastSelectionStart;
const endPos = hasFocus ? textarea.selectionEnd : this.lastSelectionStart;
textarea.value =
textarea.value.substring(0, startPos) +
colons +
textarea.value.substring(endPos, textarea.value.length);
const newPos = startPos + colons.length;
textarea.selectionStart = newPos;
textarea.selectionEnd = newPos;
this.forceUpdateLastSelectionStart(newPos);
this.forceUpdateMessageFieldSize({});
},
async setQuoteMessage(messageId) {
this.quote = null;
this.quotedMessage = null;
@ -1858,7 +1764,6 @@
}
if (!this.quotedMessage) {
this.view.restoreBottomOffset();
this.updateMessageFieldSize({});
return;
}
@ -1890,18 +1795,39 @@
}),
onInitialRender: () => {
this.view.restoreBottomOffset();
this.updateMessageFieldSize({});
},
});
},
async sendMessage(e) {
async sendMessage(message = '', options = {}) {
this.sendStart = Date.now();
try {
const contacts = await this.getUntrustedContacts(options);
this.disableMessageField();
if (contacts && contacts.length) {
const sendAnyway = await this.showSendAnywayDialog(contacts);
if (sendAnyway) {
this.sendMessage(message, { force: true });
return;
}
this.focusMessageFieldAndClearDisabled();
return;
}
} catch (error) {
this.focusMessageFieldAndClearDisabled();
window.log.error(
'sendMessage error:',
error && error.stack ? error.stack : error
);
return;
}
this.removeLastSeenIndicator();
this.model.clearTypingTimers();
const input = this.$messageField;
const message = window.Signal.Emojis.replaceColons(input.val()).trim();
let toast;
if (extension.expired()) {
toast = new Whisper.ExpiredToast();
@ -1942,11 +1868,9 @@
this.getLinkPreview()
);
input.val('');
this.compositionApi.current.reset();
this.setQuoteMessage(null);
this.resetLinkPreview();
this.focusMessageFieldAndClearDisabled();
this.forceUpdateMessageFieldSize(e);
this.fileInput.clearAttachments();
} catch (error) {
window.log.error(
@ -1958,24 +1882,16 @@
}
},
onKeyUp() {
this.maybeBumpTyping();
this.debouncedMaybeGrabLinkPreview();
onEditorStateChange(messageText, caretLocation) {
this.maybeBumpTyping(messageText);
this.debouncedMaybeGrabLinkPreview(messageText, caretLocation);
},
updateLastSelectionStart(newPos) {
if (document.activeElement === this.$messageField[0]) {
this.forceUpdateLastSelectionStart(newPos);
}
onEditorSizeChange() {
this.view.scrollToBottomIfNeeded();
},
forceUpdateLastSelectionStart(
newPos = this.$messageField[0].selectionStart
) {
this.lastSelectionStart = newPos;
},
maybeGrabLinkPreview() {
maybeGrabLinkPreview(message, caretLocation) {
// Don't generate link previews if user has turned them off
if (!storage.get('linkPreviews', false)) {
return;
@ -1993,10 +1909,7 @@
return;
}
const messageText = this.$messageField.val().trim();
const caretLocation = this.$messageField.get(0).selectionStart;
if (!messageText) {
if (!message) {
this.resetLinkPreview();
return;
}
@ -2005,7 +1918,7 @@
}
const links = window.Signal.LinkPreviews.findLinks(
messageText,
message,
caretLocation
);
const { currentlyMatchedLink } = this;
@ -2310,7 +2223,6 @@
}
if (!this.currentlyMatchedLink) {
this.view.restoreBottomOffset();
this.updateMessageFieldSize({});
return;
}
@ -2332,7 +2244,6 @@
props,
onInitialRender: () => {
this.view.restoreBottomOffset();
this.updateMessageFieldSize({});
},
});
},
@ -2362,59 +2273,12 @@
// Called whenever the user changes the message composition field. But only
// fires if there's content in the message field after the change.
maybeBumpTyping() {
const messageText = this.$messageField.val();
maybeBumpTyping(messageText) {
if (messageText.length) {
this.model.throttledBumpTyping();
}
},
updateMessageFieldSize(event) {
const keyCode = event.which || event.keyCode;
if (
keyCode === 13 &&
!event.altKey &&
!event.shiftKey &&
!event.ctrlKey
) {
// enter pressed - submit the form now
event.preventDefault();
this.$('.bottom-bar form').submit();
return;
}
this.toggleMicrophone();
this.view.measureScrollPosition();
window.autosize(this.$messageField);
const $attachmentPreviews = this.$('.attachment-previews');
const $bottomBar = this.$('.bottom-bar');
const includeMargin = true;
const quoteHeight = this.quoteView
? this.quoteView.$el.outerHeight(includeMargin)
: 0;
const height =
this.$messageField.outerHeight() +
$attachmentPreviews.outerHeight() +
quoteHeight +
parseInt($bottomBar.css('min-height'), 10);
$bottomBar.outerHeight(height);
this.view.scrollToBottomIfNeeded();
},
forceUpdateMessageFieldSize(event) {
if (this.isHidden()) {
return;
}
this.view.scrollToBottomIfNeeded();
window.autosize.update(this.$messageField);
this.updateMessageFieldSize(event);
},
isHidden() {
return (
this.$el.css('display') === 'none' ||

View File

@ -21,6 +21,7 @@
Whisper.ConversationStack = Whisper.View.extend({
className: 'conversation-stack',
lastConversation: null,
open(conversation) {
const id = `conversation-${conversation.cid}`;
if (id !== this.el.firstChild.id) {
@ -42,6 +43,10 @@
$el.prependTo(this.el);
}
conversation.trigger('opened');
if (this.lastConversation) {
this.lastConversation.trigger('backgrounded');
}
this.lastConversation = conversation;
// Make sure poppers are positioned properly
window.dispatchEvent(new Event('resize'));
},

View File

@ -53,17 +53,19 @@
"bunyan": "1.8.12",
"classnames": "2.2.5",
"config": "1.28.1",
"draft-js": "0.10.5",
"electron-context-menu": "0.11.0",
"electron-editor-context-menu": "1.1.1",
"electron-is-dev": "0.3.0",
"emoji-datasource": "4.1.0",
"emoji-datasource-apple": "4.1.0",
"emoji-js": "3.4.0",
"emoji-regex": "8.0.0",
"filesize": "3.6.1",
"firstline": "1.2.1",
"form-data": "2.3.2",
"fs-extra": "5.0.0",
"fuse.js": "^3.4.4",
"fuse.js": "3.4.4",
"glob": "7.1.2",
"google-libphonenumber": "3.2.2",
"got": "8.2.0",
@ -90,7 +92,8 @@
"react": "16.8.3",
"react-contextmenu": "2.11.0",
"react-dom": "16.8.3",
"react-popper": "^1.3.3",
"react-measure": "2.3.0",
"react-popper": "1.3.3",
"react-redux": "6.0.1",
"react-virtualized": "9.21.0",
"read-last-lines": "1.3.0",
@ -113,6 +116,7 @@
"@types/chai": "4.1.2",
"@types/classnames": "2.2.3",
"@types/config": "0.0.34",
"@types/draft-js": "0.10.32",
"@types/filesize": "3.6.0",
"@types/fs-extra": "5.0.5",
"@types/google-libphonenumber": "7.4.14",
@ -128,6 +132,7 @@
"@types/qs": "6.5.1",
"@types/react": "16.8.5",
"@types/react-dom": "16.8.2",
"@types/react-measure": "2.0.5",
"@types/react-redux": "7.0.1",
"@types/react-virtualized": "9.18.12",
"@types/redux-logger": "3.0.7",

View File

@ -62,6 +62,11 @@ module.exports = {
type: 'text/css',
href: '/stylesheets/manifest.css',
},
{
rel: 'stylesheet',
type: 'text/css',
href: '/node_modules/draft-js/dist/Draft.css',
},
],
},
},

View File

@ -2988,6 +2988,23 @@
.module-spinner__arc--small {
-webkit-mask: url('../images/spinner-24.svg') no-repeat center;
-webkit-mask-size: 100%;
height: 24px;
width: 24px;
}
.module-spinner__container--mini {
height: 14px;
width: 14px;
}
.module-spinner__circle--mini {
-webkit-mask: url('../images/spinner-track-24.svg') no-repeat center;
-webkit-mask-size: 100%;
height: 14px;
width: 14px;
}
.module-spinner__arc--mini {
-webkit-mask: url('../images/spinner-24.svg') no-repeat center;
-webkit-mask-size: 100%;
}
.module-spinner__circle--incoming {
@ -4540,6 +4557,11 @@
&--#{$size} {
width: $size;
height: $size;
&--inline {
display: inline-block;
vertical-align: bottom;
background-size: $size $size;
}
}
&__image--#{$size} {
width: $size;
@ -4550,17 +4572,23 @@
.module-emoji {
display: block;
color: transparent;
@include light-theme() {
caret-color: $color-gray-90;
}
@include dark-theme() {
caret-color: $color-gray-05;
}
@include emoji-size(16px);
@include emoji-size(18px);
@include emoji-size(20px);
@include emoji-size(28px);
@include emoji-size(32px);
@include emoji-size(64px);
@include emoji-size(66px);
&--inline {
display: inline-block;
}
}
// Module: Unsupported Message
@ -4649,6 +4677,147 @@
stroke-width: 2;
}
// Module: CompositionInput
.module-composition-input {
&__input {
line-height: 20px;
border: 1px solid;
border-radius: 18px;
font-size: 14px;
font-family: Roboto;
overflow: hidden;
word-break: break-word;
&__scroller {
padding: 7px 12px;
min-height: 32px;
max-height: 80px;
overflow: auto;
}
@include light-theme() {
border-color: $color-gray-15;
background: $color-white;
color: $color-gray-90;
}
@include dark-theme() {
border-color: $color-gray-60;
background: $color-dark-85;
color: $color-gray-05;
}
&:focus-within {
@include light-theme() {
border-color: $color-signal-blue;
}
@include dark-theme() {
border-color: $color-signal-blue;
}
}
// Override draft.js styles
.public-DraftEditorPlaceholder-root {
@include light-theme() {
color: $color-gray-45;
}
@include dark-theme() {
color: $color-gray-45;
}
}
}
&__emoji-suggestions {
padding: 12px 0;
margin-bottom: 6px;
border-radius: 8px;
z-index: 2;
@include popper-shadow();
@include light-theme() {
background: $color-white;
}
@include dark-theme() {
background: $color-gray-75;
}
&__row {
height: 30px;
padding: 0 12px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: flex-start;
background: none;
border: none;
width: 100%;
font-size: 13px;
font-family: Roboto;
@include light-theme() {
color: $color-gray-60;
}
@include dark-theme() {
color: $color-gray-25;
}
&__short-name {
margin-left: 4px;
}
&--selected,
&:hover {
@include light-theme() {
background: $color-gray-10;
color: $color-gray-90;
}
@include dark-theme() {
background: $color-gray-60;
color: $color-gray-05;
}
}
}
stroke: $color-white;
}
}
// Module: CompositionArea
.module-composition-area {
// Layout
display: flex;
flex-direction: row;
// Child Elements
&__button-cell {
display: flex;
justify-content: center;
align-items: center;
width: 44px;
height: 100%;
flex-shrink: 0;
&--microphone-active {
width: 100px;
}
}
&__input {
flex-grow: 1;
}
}
.composition-area-placeholder {
flex-grow: 1;
margin: {
top: 3px;
bottom: 6px;
}
}
// Third-party module: react-contextmenu
.react-contextmenu {

View File

@ -0,0 +1,29 @@
#### Default
```jsx
<util.ConversationContext theme={util.theme}>
<div style={{ minHeight: '500px', paddingTop: '450px' }}>
<CompositionArea
i18n={util.i18n}
onSubmit={s => console.log('onSubmit', s)}
onDirtyChange={dirty =>
console.log(`Dirty Change: ${dirty ? 'dirty' : 'not dirty'}`)
}
// EmojiButton
onSetSkinTone={s => console.log('onSetSkinTone', s)}
// StickerButton
knownPacks={[]}
receivedPacks={[]}
installedPacks={[]}
blessedPacks={[]}
recentStickers={[]}
clearInstalledStickerPack={() => console.log('clearInstalledStickerPack')}
onClickAddPack={(...args) => console.log('onClickAddPack', ...args)}
onPickSticker={(...args) => console.log('onPickSticker', ...args)}
clearShowIntroduction={() => console.log('clearShowIntroduction')}
showPickerHint={false}
clearShowPickerHint={() => console.log('clearShowIntroduction')}
/>
</div>
</util.ConversationContext>
```

View File

@ -0,0 +1,182 @@
import * as React from 'react';
import { Editor } from 'draft-js';
import {
EmojiButton,
EmojiPickDataType,
Props as EmojiButtonProps,
} from './emoji/EmojiButton';
import {
Props as StickerButtonProps,
StickerButton,
} from './stickers/StickerButton';
import {
CompositionInput,
InputApi,
Props as CompositionInputProps,
} from './CompositionInput';
import { countStickers } from './stickers/lib';
import { LocalizerType } from '../types/Util';
export type OwnProps = {
readonly i18n: LocalizerType;
readonly compositionApi?: React.MutableRefObject<{
focusInput: () => void;
setDisabled: (disabled: boolean) => void;
reset: InputApi['reset'];
resetEmojiResults: InputApi['resetEmojiResults'];
}>;
};
export type Props = CompositionInputProps &
Pick<
EmojiButtonProps,
'onPickEmoji' | 'onSetSkinTone' | 'recentEmojis' | 'skinTone'
> &
Pick<
StickerButtonProps,
| 'knownPacks'
| 'receivedPacks'
| 'installedPacks'
| 'blessedPacks'
| 'recentStickers'
| 'clearInstalledStickerPack'
| 'onClickAddPack'
| 'onPickSticker'
| 'clearShowIntroduction'
| 'showPickerHint'
| 'clearShowPickerHint'
> &
OwnProps;
// tslint:disable-next-line max-func-body-length
export const CompositionArea = ({
i18n,
// CompositionInput
onDirtyChange,
onSubmit,
compositionApi,
onEditorSizeChange,
onEditorStateChange,
// EmojiButton
onPickEmoji,
onSetSkinTone,
recentEmojis,
skinTone,
// StickerButton
knownPacks,
receivedPacks,
installedPacks,
blessedPacks,
recentStickers,
clearInstalledStickerPack,
onClickAddPack,
onPickSticker,
clearShowIntroduction,
showPickerHint,
clearShowPickerHint,
}: Props) => {
const [disabled, setDisabled] = React.useState(false);
const editorRef = React.useRef<Editor>(null);
const inputApiRef = React.useRef<InputApi | undefined>();
const handleForceSend = React.useCallback(
() => {
if (inputApiRef.current) {
inputApiRef.current.submit();
}
},
[inputApiRef]
);
const focusInput = React.useCallback(
() => {
if (editorRef.current) {
editorRef.current.focus();
}
},
[editorRef]
);
const withStickers =
countStickers({
knownPacks,
blessedPacks,
installedPacks,
receivedPacks,
}) > 0;
if (compositionApi) {
compositionApi.current = {
focusInput,
setDisabled,
reset: () => {
if (inputApiRef.current) {
inputApiRef.current.reset();
}
},
resetEmojiResults: () => {
if (inputApiRef.current) {
inputApiRef.current.resetEmojiResults();
}
},
};
}
const insertEmoji = React.useCallback(
(e: EmojiPickDataType) => {
if (inputApiRef.current) {
inputApiRef.current.insertEmoji(e);
onPickEmoji(e);
}
},
[inputApiRef, onPickEmoji]
);
return (
<div className="module-composition-area">
<div className="module-composition-area__button-cell">
<EmojiButton
i18n={i18n}
doSend={handleForceSend}
onPickEmoji={insertEmoji}
recentEmojis={recentEmojis}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}
onClose={focusInput}
/>
</div>
<div className="module-composition-area__input">
<CompositionInput
i18n={i18n}
disabled={disabled}
editorRef={editorRef}
inputApi={inputApiRef}
onPickEmoji={onPickEmoji}
onSubmit={onSubmit}
onEditorSizeChange={onEditorSizeChange}
onEditorStateChange={onEditorStateChange}
onDirtyChange={onDirtyChange}
skinTone={skinTone}
/>
</div>
{withStickers ? (
<div className="module-composition-area__button-cell">
<StickerButton
i18n={i18n}
knownPacks={knownPacks}
receivedPacks={receivedPacks}
installedPacks={installedPacks}
blessedPacks={blessedPacks}
recentStickers={recentStickers}
clearInstalledStickerPack={clearInstalledStickerPack}
onClickAddPack={onClickAddPack}
onPickSticker={onPickSticker}
clearShowIntroduction={clearShowIntroduction}
showPickerHint={showPickerHint}
clearShowPickerHint={clearShowPickerHint}
/>
</div>
) : null}
</div>
);
};

View File

@ -0,0 +1,12 @@
#### Default
```jsx
<util.ConversationContext theme={util.theme}>
<div style={{ minHeight: '500px', paddingTop: '450px' }}>
<CompositionInput
i18n={util.i18n}
onSubmit={s => console.log('onSubmit', s)}
/>
</div>
</util.ConversationContext>
```

View File

@ -0,0 +1,693 @@
import * as React from 'react';
import { createPortal } from 'react-dom';
import { createSelector } from 'reselect';
import {
CompositeDecorator,
ContentBlock,
ContentState,
DraftEditorCommand,
DraftHandleValue,
Editor,
EditorChangeType,
EditorState,
getDefaultKeyBinding,
Modifier,
SelectionState,
} from 'draft-js';
import Measure, { ContentRect } from 'react-measure';
import { Manager, Popper, Reference } from 'react-popper';
import { clamp, noop } from 'lodash';
import classNames from 'classnames';
import emojiRegex from 'emoji-regex';
import { Emoji } from './emoji/Emoji';
import { EmojiPickDataType } from './emoji/EmojiPicker';
import {
convertShortName,
EmojiData,
replaceColons,
search,
} from './emoji/lib';
import { LocalizerType } from '../types/Util';
const colonsRegex = /(?:^|\s):[a-z0-9-_+]+:?/gi;
export type Props = {
readonly i18n: LocalizerType;
readonly disabled?: boolean;
readonly editorRef?: React.RefObject<Editor>;
readonly inputApi?: React.MutableRefObject<InputApi | undefined>;
readonly skinTone?: EmojiPickDataType['skinTone'];
onDirtyChange?(dirty: boolean): unknown;
onEditorStateChange?(messageText: string, caretLocation: number): unknown;
onEditorSizeChange?(rect: ContentRect): unknown;
onPickEmoji(o: EmojiPickDataType): unknown;
onSubmit(message: string): unknown;
};
export type InputApi = {
insertEmoji: (e: EmojiPickDataType) => void;
reset: () => void;
resetEmojiResults: () => void;
submit: () => void;
};
export type CompositionInputEditorCommand =
| DraftEditorCommand
| ('enter-emoji' | 'next-emoji' | 'prev-emoji' | 'submit');
function getTrimmedMatchAtIndex(str: string, index: number, pattern: RegExp) {
let match;
// Reset regex state
pattern.exec('');
// tslint:disable-next-line no-conditional-assignment
while ((match = pattern.exec(str))) {
const matchStr = match.toString();
const start = match.index + (matchStr.length - matchStr.trimLeft().length);
const end = match.index + matchStr.trimRight().length;
if (index >= start && index <= end) {
return match.toString();
}
}
return null;
}
function getWordAtIndex(str: string, index: number) {
const start = str
.slice(0, index + 1)
.replace(/\s+$/, '')
.search(/\S+$/);
const end = str.slice(index).search(/(?:\s|$)/) + index;
return {
start,
end,
word: str.slice(start, end),
};
}
const compositeDecorator = new CompositeDecorator([
{
strategy: (block, cb) => {
const pat = emojiRegex();
const text = block.getText();
let match;
let index;
// tslint:disable-next-line no-conditional-assignment
while ((match = pat.exec(text))) {
index = match.index;
cb(index, index + match[0].length);
}
},
component: ({
children,
contentState,
entityKey,
}: {
children: React.ReactNode;
contentState: ContentState;
entityKey: string;
}) =>
entityKey ? (
<Emoji
shortName={contentState.getEntity(entityKey).getData().shortName}
skinTone={contentState.getEntity(entityKey).getData().skinTone}
inline={true}
size={20}
>
{children}
</Emoji>
) : (
children
),
},
]);
type FunctionRef = (el: HTMLElement | null) => unknown;
// A selector which combines multiple react refs into a single, referentially-equal functional ref.
const combineRefs = createSelector(
(r1: FunctionRef) => r1,
(_r1: any, r2: FunctionRef) => r2,
(_r1: any, _r2: any, r3: React.MutableRefObject<HTMLDivElement>) => r3,
(r1, r2, r3) => (el: HTMLDivElement) => {
r1(el);
r2(el);
r3.current = el;
}
);
// tslint:disable-next-line max-func-body-length
export const CompositionInput = ({
i18n,
disabled,
editorRef,
inputApi,
onDirtyChange,
onEditorStateChange,
onEditorSizeChange,
onPickEmoji,
onSubmit,
skinTone,
}: Props) => {
const [editorState, setEditorState] = React.useState(
EditorState.createEmpty(compositeDecorator)
);
const [searchText, setSearchText] = React.useState<string>('');
const [emojiResults, setEmojiResults] = React.useState<Array<EmojiData>>([]);
const [emojiResultsIndex, setEmojiResultsIndex] = React.useState<number>(0);
const [editorWidth, setEditorWidth] = React.useState<number>(0);
const [popperRoot, setPopperRoot] = React.useState<HTMLDivElement | null>(
null
);
const dirtyRef = React.useRef(false);
const focusRef = React.useRef(false);
const editorStateRef = React.useRef<EditorState>(editorState);
const rootElRef = React.useRef<HTMLDivElement>();
// This function sets editorState and also keeps a reference to the newly set
// state so we can reference the state in effects and callbacks without
// excessive cleanup
const setAndTrackEditorState = React.useCallback(
(newState: EditorState) => {
setEditorState(newState);
editorStateRef.current = newState;
},
[setEditorState, editorStateRef]
);
const updateExternalStateListeners = React.useCallback(
(newState: EditorState) => {
const plainText = newState.getCurrentContent().getPlainText();
const currentBlockKey = newState.getSelection().getStartKey();
const currentBlockIndex = editorState
.getCurrentContent()
.getBlockMap()
.keySeq()
.findIndex(key => key === currentBlockKey);
const caretLocation = newState
.getCurrentContent()
.getBlockMap()
.valueSeq()
.toArray()
.reduce((sum: number, block: ContentBlock, index: number) => {
if (currentBlockIndex < index) {
return sum + block.getText().length + 1; // +1 for newline
}
if (currentBlockIndex === index) {
return sum + newState.getSelection().getStartOffset();
}
return sum;
}, 0);
if (onDirtyChange) {
const isDirty = !!plainText;
if (dirtyRef.current !== isDirty) {
dirtyRef.current = isDirty;
onDirtyChange(isDirty);
}
}
if (onEditorStateChange) {
onEditorStateChange(plainText, caretLocation);
}
},
[onDirtyChange, onEditorStateChange]
);
const resetEmojiResults = React.useCallback(
() => {
setEmojiResults([]);
setEmojiResultsIndex(0);
setSearchText('');
},
[setEmojiResults, setEmojiResultsIndex, setSearchText]
);
const handleEditorStateChange = React.useCallback(
(newState: EditorState) => {
// Does the current position have any emojiable text?
const selection = newState.getSelection();
const caretLocation = selection.getStartOffset();
const content = newState
.getCurrentContent()
.getBlockForKey(selection.getAnchorKey())
.getText();
const match = getTrimmedMatchAtIndex(content, caretLocation, colonsRegex);
// Update the state to indicate emojiable text at the current position.
const newSearchText = match ? match.trim().substr(1) : '';
if (newSearchText.length >= 2 && focusRef.current) {
setEmojiResults(search(newSearchText, 10));
setSearchText(newSearchText);
setEmojiResultsIndex(0);
} else {
resetEmojiResults();
}
// Finally, update the editor state
setAndTrackEditorState(newState);
updateExternalStateListeners(newState);
},
[
focusRef,
resetEmojiResults,
setAndTrackEditorState,
setSearchText,
setEmojiResults,
]
);
const resetEditorState = React.useCallback(
() => {
const newEmptyState = EditorState.createEmpty(compositeDecorator);
setAndTrackEditorState(newEmptyState);
resetEmojiResults();
},
[editorStateRef, resetEmojiResults, setAndTrackEditorState]
);
const submit = React.useCallback(
() => {
const text = editorState.getCurrentContent().getPlainText();
const emojidText = replaceColons(text);
onSubmit(emojidText);
},
[editorState, onSubmit]
);
const handleEditorSizeChange = React.useCallback(
(rect: ContentRect) => {
if (rect.bounds) {
setEditorWidth(rect.bounds.width);
if (onEditorSizeChange) {
onEditorSizeChange(rect);
}
}
},
[onEditorSizeChange, setEditorWidth]
);
const selectEmojiResult = React.useCallback(
(dir: 'next' | 'prev', e?: React.KeyboardEvent) => {
if (emojiResults.length > 0) {
if (e) {
e.preventDefault();
}
if (dir === 'next') {
setEmojiResultsIndex(
clamp(emojiResultsIndex + 1, 0, emojiResults.length - 1)
);
}
if (dir === 'prev') {
setEmojiResultsIndex(
clamp(emojiResultsIndex - 1, 0, emojiResults.length - 1)
);
}
}
},
[setEmojiResultsIndex, emojiResultsIndex, emojiResults]
);
const handleEditorArrowKey = React.useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'ArrowUp') {
selectEmojiResult('prev', e);
}
if (e.key === 'ArrowDown') {
selectEmojiResult('next', e);
}
},
[selectEmojiResult]
);
const handleEscapeKey = React.useCallback(
(e: React.KeyboardEvent) => {
if (emojiResults.length > 0) {
e.preventDefault();
resetEmojiResults();
}
},
[resetEmojiResults, emojiResults]
);
const getWordAtCaret = React.useCallback(
() => {
const selection = editorState.getSelection();
const index = selection.getAnchorOffset();
return getWordAtIndex(
editorState
.getCurrentContent()
.getBlockForKey(selection.getAnchorKey())
.getText(),
index
);
},
[editorState]
);
const insertEmoji = React.useCallback(
(e: EmojiPickDataType, replaceWord: boolean = false) => {
const selection = editorState.getSelection();
const oldContent = editorState.getCurrentContent();
const emojiContent = convertShortName(e.shortName, e.skinTone);
const emojiEntityKey = oldContent
.createEntity('emoji', 'IMMUTABLE', {
shortName: e.shortName,
skinTone: e.skinTone,
})
.getLastCreatedEntityKey();
const word = getWordAtCaret();
let newContent = replaceWord
? Modifier.replaceText(
oldContent,
selection.merge({
anchorOffset: word.start,
focusOffset: word.end,
}) as SelectionState,
emojiContent,
undefined,
emojiEntityKey
)
: Modifier.insertText(
oldContent,
selection,
emojiContent,
undefined,
emojiEntityKey
);
const afterSelection = newContent.getSelectionAfter();
if (
afterSelection.getAnchorOffset() ===
newContent.getBlockForKey(afterSelection.getAnchorKey()).getLength()
) {
newContent = Modifier.insertText(newContent, afterSelection, ' ');
}
const newState = EditorState.push(
editorState,
newContent,
'insert-emoji' as EditorChangeType
);
setAndTrackEditorState(newState);
resetEmojiResults();
},
[editorState, setAndTrackEditorState, resetEmojiResults]
);
const handleEditorCommand = React.useCallback(
(
command: CompositionInputEditorCommand,
state: EditorState
): DraftHandleValue => {
if (command === 'enter-emoji') {
const shortName = emojiResults[emojiResultsIndex].short_name;
const content = state.getCurrentContent();
const selection = state.getSelection();
const word = getWordAtCaret();
const emojiContent = convertShortName(shortName, skinTone);
const emojiEntityKey = content
.createEntity('emoji', 'IMMUTABLE', {
shortName,
skinTone,
})
.getLastCreatedEntityKey();
const replaceSelection = selection.merge({
anchorOffset: word.start,
focusOffset: word.end,
});
let newContent = Modifier.replaceText(
content,
replaceSelection as SelectionState,
emojiContent,
undefined,
emojiEntityKey
);
const afterSelection = newContent.getSelectionAfter();
if (
afterSelection.getAnchorOffset() ===
newContent.getBlockForKey(afterSelection.getAnchorKey()).getLength()
) {
newContent = Modifier.insertText(newContent, afterSelection, ' ');
}
const newState = EditorState.push(
state,
newContent,
'insert-emoji' as EditorChangeType
);
setAndTrackEditorState(newState);
resetEmojiResults();
onPickEmoji({ shortName });
return 'handled';
}
if (command === 'submit') {
submit();
return 'handled';
}
if (command === 'next-emoji') {
selectEmojiResult('next');
}
if (command === 'prev-emoji') {
selectEmojiResult('prev');
}
return 'not-handled';
},
[
emojiResults,
emojiResultsIndex,
resetEmojiResults,
selectEmojiResult,
setAndTrackEditorState,
skinTone,
submit,
]
);
const onTab = React.useCallback(
(e: React.KeyboardEvent) => {
if (e.shiftKey || emojiResults.length === 0) {
return;
}
e.preventDefault();
handleEditorCommand('enter-emoji', editorState);
},
[emojiResults, editorState, handleEditorCommand, resetEmojiResults]
);
const editorKeybindingFn = React.useCallback(
(e: React.KeyboardEvent): CompositionInputEditorCommand | null => {
if (e.key === 'Enter' && emojiResults.length > 0) {
e.preventDefault();
return 'enter-emoji';
}
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
return 'submit';
}
if (e.key === 'n' && e.ctrlKey) {
e.preventDefault();
return 'next-emoji';
}
if (e.key === 'p' && e.ctrlKey) {
e.preventDefault();
return 'prev-emoji';
}
return getDefaultKeyBinding(e);
},
[emojiResults]
);
// Create popper root
React.useEffect(
() => {
if (emojiResults.length > 0) {
const root = document.createElement('div');
setPopperRoot(root);
document.body.appendChild(root);
return () => {
document.body.removeChild(root);
setPopperRoot(null);
};
}
return noop;
},
[setPopperRoot, emojiResults]
);
const onFocus = React.useCallback(
() => {
focusRef.current = true;
},
[focusRef]
);
const onBlur = React.useCallback(
() => {
focusRef.current = false;
},
[focusRef]
);
// Manage focus
// Chromium places the editor caret at the beginning of contenteditable divs on focus
// Here, we force the last known selection on focusin (doing this with onFocus wasn't behaving properly)
// This needs to be done in an effect because React doesn't support focus{In,Out}
// https://github.com/facebook/react/issues/6410
React.useLayoutEffect(
() => {
const { current: rootEl } = rootElRef;
if (rootEl) {
const onFocusIn = () => {
const { current: oldState } = editorStateRef;
// Force selection to be old selection
setAndTrackEditorState(
EditorState.forceSelection(oldState, oldState.getSelection())
);
};
rootEl.addEventListener('focusin', onFocusIn);
return () => {
rootEl.removeEventListener('focusin', onFocusIn);
};
}
return noop;
},
[editorStateRef, rootElRef, setAndTrackEditorState]
);
if (inputApi) {
inputApi.current = {
reset: resetEditorState,
submit,
insertEmoji,
resetEmojiResults,
};
}
return (
<Manager>
<Reference>
{({ ref: popperRef }) => (
<Measure bounds={true} onResize={handleEditorSizeChange}>
{({ measureRef }) => (
<div
className="module-composition-input__input"
ref={combineRefs(popperRef, measureRef, rootElRef)}
>
<div className="module-composition-input__input__scroller">
<Editor
ref={editorRef}
editorState={editorState}
onChange={handleEditorStateChange}
placeholder={i18n('sendMessage')}
onUpArrow={handleEditorArrowKey}
onDownArrow={handleEditorArrowKey}
onEscape={handleEscapeKey}
onTab={onTab}
handleKeyCommand={handleEditorCommand}
keyBindingFn={editorKeybindingFn}
spellCheck={true}
stripPastedStyles={true}
readOnly={disabled}
onFocus={onFocus}
onBlur={onBlur}
/>
</div>
</div>
)}
</Measure>
)}
</Reference>
{emojiResults.length > 0 && popperRoot
? createPortal(
<Popper placement="top" key={searchText}>
{({ ref, style }) => (
<div
ref={ref}
className="module-composition-input__emoji-suggestions"
style={{
...style,
width: editorWidth,
}}
role="listbox"
aria-expanded={true}
aria-activedescendant={`emoji-result--${
emojiResults[emojiResultsIndex].short_name
}`}
>
{emojiResults.map((emoji, index) => (
<button
key={emoji.short_name}
id={`emoji-result--${emoji.short_name}`}
role="option button"
aria-selected={emojiResultsIndex === index}
onMouseDown={() => {
insertEmoji(
{ shortName: emoji.short_name, skinTone },
true
);
onPickEmoji({ shortName: emoji.short_name });
}}
className={classNames(
'module-composition-input__emoji-suggestions__row',
emojiResultsIndex === index
? 'module-composition-input__emoji-suggestions__row--selected'
: null
)}
>
<Emoji
shortName={emoji.short_name}
size={16}
skinTone={skinTone}
/>
<div className="module-composition-input__emoji-suggestions__row__short-name">
:{emoji.short_name}:
</div>
</button>
))}
</div>
)}
</Popper>,
popperRoot
)
: null}
</Manager>
);
};

View File

@ -60,7 +60,7 @@ export class ContactListItem extends React.Component<Props> {
const profileElement =
!isMe && profileName && !name ? (
<span className="module-contact-list-item__text__profile-name">
~<Emojify text={profileName} i18n={i18n} />
~<Emojify text={profileName} />
</span>
) : null;
@ -79,7 +79,7 @@ export class ContactListItem extends React.Component<Props> {
{this.renderAvatar()}
<div className="module-contact-list-item__text">
<div className="module-contact-list-item__text__name">
<Emojify text={displayName} i18n={i18n} /> {profileElement}
<Emojify text={displayName} /> {profileElement}
</div>
<div className="module-contact-list-item__text__additional-data">
{showVerified ? (

View File

@ -111,7 +111,6 @@ export class ConversationListItem extends React.PureComponent<Props> {
phoneNumber={phoneNumber}
name={name}
profileName={profileName}
i18n={i18n}
/>
)}
</div>

View File

@ -18,7 +18,6 @@ const renderNewLines: RenderTextCallbackType = ({ text, key }) => (
);
const renderEmoji = ({
i18n,
text,
key,
sizeClass,
@ -31,7 +30,6 @@ const renderEmoji = ({
renderNonEmoji: RenderTextCallbackType;
}) => (
<Emojify
i18n={i18n}
key={key}
text={text}
sizeClass={sizeClass}

View File

@ -66,7 +66,6 @@ export class MessageSearchResult extends React.PureComponent<Props> {
phoneNumber={from.phoneNumber}
name={from.name}
profileName={from.profileName}
i18n={i18n}
module="module-message-search-result__header__name"
/>
);
@ -85,7 +84,6 @@ export class MessageSearchResult extends React.PureComponent<Props> {
phoneNumber={to.phoneNumber}
name={to.name}
profileName={to.profileName}
i18n={i18n}
/>
</span>
</div>

View File

@ -2,7 +2,6 @@
```jsx
<ContactName
i18n={util.i18n}
name="Someone 🔥 Somewhere"
phoneNumber="(202) 555-0011"
profileName="🔥Flames🔥"
@ -12,15 +11,11 @@
#### Number and profile, no name
```jsx
<ContactName
i18n={util.i18n}
phoneNumber="(202) 555-0011"
profileName="🔥Flames🔥"
/>
<ContactName phoneNumber="(202) 555-0011" profileName="🔥Flames🔥" />
```
#### No name, no profile
```jsx
<ContactName i18n={util.i18n} phoneNumber="(202) 555-0011" />
<ContactName phoneNumber="(202) 555-0011" />
```

View File

@ -2,32 +2,29 @@ import React from 'react';
import { Emojify } from './Emojify';
import { LocalizerType } from '../../types/Util';
interface Props {
phoneNumber: string;
name?: string;
profileName?: string;
i18n: LocalizerType;
module?: string;
}
export class ContactName extends React.Component<Props> {
public render() {
const { phoneNumber, name, profileName, i18n, module } = this.props;
const { phoneNumber, name, profileName, module } = this.props;
const prefix = module ? module : 'module-contact-name';
const title = name ? name : phoneNumber;
const shouldShowProfile = Boolean(profileName && !name);
const profileElement = shouldShowProfile ? (
<span className={`${prefix}__profile-name`}>
~<Emojify text={profileName || ''} i18n={i18n} />
~<Emojify text={profileName || ''} />
</span>
) : null;
return (
<span className={prefix} dir="auto">
<Emojify text={title} i18n={i18n} />
<Emojify text={title} />
{shouldShowProfile ? ' ' : null}
{profileElement}
</span>

View File

@ -101,12 +101,12 @@ export class ConversationHeader extends React.Component<Props> {
return (
<div className="module-conversation-header__title">
{name ? <Emojify text={name} i18n={i18n} /> : null}
{name ? <Emojify text={name} /> : null}
{name && phoneNumber ? ' · ' : null}
{phoneNumber ? phoneNumber : null}{' '}
{profileName && !name ? (
<span className="module-conversation-header__title__profile-name">
~<Emojify text={profileName} i18n={i18n} />
~<Emojify text={profileName} />
</span>
) : null}
{isVerified ? ' · ' : null}

View File

@ -1,53 +1,53 @@
### All emoji
```jsx
<Emojify text="🔥🔥🔥" i18n={util.i18n} />
<Emojify text="🔥🔥🔥" />
```
### With skin color modifier
```jsx
<Emojify text="👍🏾" i18n={util.i18n} />
<Emojify text="👍🏾" />
```
### With `sizeClass` provided
```jsx
<Emojify text="🔥" sizeClass="jumbo" i18n={util.i18n} />
<Emojify text="🔥" sizeClass="jumbo" />
```
```jsx
<Emojify text="🔥" sizeClass="large" i18n={util.i18n} />
<Emojify text="🔥" sizeClass="large" />
```
```jsx
<Emojify text="🔥" sizeClass="medium" i18n={util.i18n} />
<Emojify text="🔥" sizeClass="medium" />
```
```jsx
<Emojify text="🔥" sizeClass="small" i18n={util.i18n} />
<Emojify text="🔥" sizeClass="small" />
```
```jsx
<Emojify text="🔥" sizeClass="" i18n={util.i18n} />
<Emojify text="🔥" sizeClass="" />
```
### Starting and ending with emoji
```jsx
<Emojify text="🔥in between🔥" i18n={util.i18n} />
<Emojify text="🔥in between🔥" />
```
### With emoji in the middle
```jsx
<Emojify text="Before 🔥🔥 after" i18n={util.i18n} />
<Emojify text="Before 🔥🔥 after" />
```
### No emoji
```jsx
<Emojify text="This is the text" i18n={util.i18n} />
<Emojify text="This is the text" />
```
### Providing custom non-link render function
@ -56,9 +56,5 @@
const renderNonEmoji = ({ text, key }) => (
<span key={key}>This is my custom content</span>
);
<Emojify
text="Before 🔥🔥 after"
renderNonEmoji={renderNonEmoji}
i18n={util.i18n}
/>;
<Emojify text="Before 🔥🔥 after" renderNonEmoji={renderNonEmoji} />;
```

View File

@ -7,23 +7,20 @@ import {
findImage,
getRegex,
getReplacementData,
getTitle,
SizeClassType,
} from '../../util/emoji';
import { LocalizerType, RenderTextCallbackType } from '../../types/Util';
import { RenderTextCallbackType } from '../../types/Util';
// Some of this logic taken from emoji-js/replacement
function getImageTag({
match,
sizeClass,
key,
i18n,
}: {
match: any;
sizeClass?: SizeClassType;
key: string | number;
i18n: LocalizerType;
}) {
const result = getReplacementData(match[0], match[1], match[2]);
@ -32,7 +29,6 @@ function getImageTag({
}
const img = findImage(result.value, result.variation);
const title = getTitle(result.value);
if (
!img.path ||
@ -46,12 +42,10 @@ function getImageTag({
<img
key={key}
src={img.path}
// We can't use alt or it will be what is captured when a user copies message
// contents ("Emoji of ':1'"). Instead, we want the title to be copied (':+1:').
aria-label={i18n('emojiAlt', [title || ''])}
aria-label={match[0]}
className={classNames('emoji', sizeClass)}
data-codepoints={img.full_idx}
title={`:${title}:`}
title={match[0]}
/>
);
}
@ -62,7 +56,6 @@ interface Props {
sizeClass?: SizeClassType;
/** Allows you to customize now non-newlines are rendered. Simplest is just a <span>. */
renderNonEmoji?: RenderTextCallbackType;
i18n: LocalizerType;
}
export class Emojify extends React.Component<Props> {
@ -71,7 +64,7 @@ export class Emojify extends React.Component<Props> {
};
public render() {
const { text, sizeClass, renderNonEmoji, i18n } = this.props;
const { text, sizeClass, renderNonEmoji } = this.props;
const results: Array<any> = [];
const regex = getRegex();
@ -95,7 +88,7 @@ export class Emojify extends React.Component<Props> {
results.push(renderNonEmoji({ text: textWithNoEmoji, key: count++ }));
}
results.push(getImageTag({ match, sizeClass, key: count++, i18n }));
results.push(getImageTag({ match, sizeClass, key: count++ }));
last = regex.lastIndex;
match = regex.exec(text);

View File

@ -45,7 +45,6 @@ export class GroupNotification extends React.Component<Props> {
className="module-group-notification__contact"
>
<ContactName
i18n={i18n}
phoneNumber={contact.phoneNumber}
profileName={contact.profileName}
name={contact.name}

View File

@ -330,7 +330,6 @@ export class Message extends React.PureComponent<Props, State> {
collapseMetadata,
conversationType,
direction,
i18n,
isSticker,
isTapToView,
isTapToViewExpired,
@ -361,7 +360,6 @@ export class Message extends React.PureComponent<Props, State> {
name={authorName}
profileName={authorProfileName}
module={moduleName}
i18n={i18n}
/>
</div>
);

View File

@ -23,7 +23,6 @@ const renderNewLines: RenderTextCallbackType = ({
}) => <AddNewLines key={key} text={textWithNewLines} />;
const renderEmoji = ({
i18n,
text,
key,
sizeClass,
@ -36,7 +35,6 @@ const renderEmoji = ({
renderNonEmoji: RenderTextCallbackType;
}) => (
<Emojify
i18n={i18n}
key={key}
text={text}
sizeClass={sizeClass}

View File

@ -111,7 +111,6 @@ export class MessageDetail extends React.Component<Props> {
phoneNumber={contact.phoneNumber}
name={contact.name}
profileName={contact.profileName}
i18n={i18n}
/>
</div>
{errors.map((error, index) => (

View File

@ -303,7 +303,6 @@ export class Quote extends React.Component<Props, State> {
phoneNumber={authorPhoneNumber}
name={authorName}
profileName={authorProfileName}
i18n={i18n}
/>
)}
</div>

View File

@ -46,7 +46,6 @@ export class SafetyNumberNotification extends React.Component<Props> {
className="module-safety-number-notification__contact"
>
<ContactName
i18n={i18n}
name={contact.name}
profileName={contact.profileName}
phoneNumber={contact.phoneNumber}

View File

@ -45,7 +45,6 @@ export class TimerNotification extends React.Component<Props> {
id={changeKey}
components={[
<ContactName
i18n={i18n}
key="external-1"
phoneNumber={phoneNumber}
profileName={profileName}

View File

@ -60,7 +60,6 @@ export class UnsupportedMessage extends React.Component<Props> {
className="module-unsupported-message__contact"
>
<ContactName
i18n={i18n}
name={contact.name}
profileName={contact.profileName}
phoneNumber={contact.phoneNumber}

View File

@ -52,7 +52,6 @@ export class VerificationNotification extends React.Component<Props> {
id={id}
components={[
<ContactName
i18n={i18n}
key="external-1"
name={contact.name}
profileName={contact.profileName}

View File

@ -6,7 +6,8 @@ export type OwnProps = {
inline?: boolean;
shortName: string;
skinTone?: SkinToneKey | number;
size?: 16 | 20 | 28 | 32 | 64 | 66;
size?: 16 | 18 | 20 | 28 | 32 | 64 | 66;
children?: React.ReactNode;
};
export type Props = OwnProps &
@ -15,28 +16,45 @@ export type Props = OwnProps &
export const Emoji = React.memo(
React.forwardRef<HTMLDivElement, Props>(
(
{ style = {}, size = 28, shortName, skinTone, inline, className }: Props,
{
style = {},
size = 28,
shortName,
skinTone,
inline,
className,
children,
}: Props,
ref
) => {
const image = getImagePath(shortName, skinTone);
const backgroundStyle = inline
? { backgroundImage: `url('${image}')` }
: {};
return (
<div
<span
ref={ref}
className={classNames(
'module-emoji',
`module-emoji--${size}px`,
inline ? 'module-emoji--inline' : null,
inline ? `module-emoji--${size}px--inline` : null,
className
)}
style={style}
style={{ ...style, ...backgroundStyle }}
>
<img
className={`module-emoji__image--${size}px`}
src={image}
alt={shortName}
/>
</div>
{inline ? (
// When using this component as a draft.js decorator it is very
// important that these children are the only elements to render
children
) : (
<img
className={`module-emoji__image--${size}px`}
src={image}
alt={shortName}
/>
)}
</span>
);
}
)

View File

@ -3,9 +3,15 @@ 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 {
EmojiPickDataType,
EmojiPicker,
Props as EmojiPickerProps,
} from './EmojiPicker';
import { LocalizerType } from '../../types/Util';
export type EmojiPickDataType = EmojiPickDataType;
export type OwnProps = {
readonly i18n: LocalizerType;
};
@ -14,22 +20,22 @@ export type Props = OwnProps &
Pick<
EmojiPickerProps,
| 'onClose'
| 'onForceSend'
| 'doSend'
| 'onPickEmoji'
| 'skinTone'
| 'onSetSkinTone'
| 'recentEmojis'
| 'skinTone'
>;
export const EmojiButton = React.memo(
({
i18n,
onClose,
onForceSend,
doSend,
onPickEmoji,
skinTone,
onSetSkinTone,
recentEmojis,
onClose,
}: Props) => {
const [open, setOpen] = React.useState(false);
const [popperRoot, setPopperRoot] = React.useState<HTMLElement | null>(
@ -49,8 +55,8 @@ export const EmojiButton = React.memo(
const handleClose = React.useCallback(
() => {
onClose();
setOpen(false);
onClose();
},
[setOpen, onClose]
);
@ -65,6 +71,7 @@ export const EmojiButton = React.memo(
const handleOutsideClick = ({ target }: MouseEvent) => {
if (!root.contains(target as Node)) {
setOpen(false);
onClose();
}
};
document.addEventListener('click', handleOutsideClick);
@ -104,7 +111,7 @@ export const EmojiButton = React.memo(
i18n={i18n}
style={style}
onPickEmoji={onPickEmoji}
onForceSend={onForceSend}
doSend={doSend}
onClose={handleClose}
skinTone={skinTone}
onSetSkinTone={onSetSkinTone}

View File

@ -19,10 +19,12 @@ import { Emoji } from './Emoji';
import { dataByCategory, search } from './lib';
import { LocalizerType } from '../../types/Util';
export type EmojiPickDataType = { skinTone?: number; shortName: string };
export type OwnProps = {
readonly i18n: LocalizerType;
readonly onPickEmoji: (o: { skinTone: number; shortName: string }) => unknown;
readonly onForceSend: () => unknown;
readonly onPickEmoji: (o: EmojiPickDataType) => unknown;
readonly doSend: () => unknown;
readonly skinTone: number;
readonly onSetSkinTone: (tone: number) => unknown;
readonly recentEmojis: Array<string>;
@ -57,7 +59,7 @@ export const EmojiPicker = React.memo(
(
{
i18n,
onForceSend,
doSend,
onPickEmoji,
skinTone = 0,
onSetSkinTone,
@ -123,7 +125,7 @@ export const EmojiPicker = React.memo(
if ('key' in e) {
if (e.key === 'Enter') {
e.preventDefault();
onForceSend();
doSend();
}
} else {
const { shortName } = e.currentTarget.dataset;
@ -132,7 +134,7 @@ export const EmojiPicker = React.memo(
}
}
},
[onClose, onForceSend, onPickEmoji, selectedTone]
[doSend, onPickEmoji, selectedTone]
);
// Handle escape key

View File

@ -9,6 +9,7 @@ import {
map,
mapValues,
sortBy,
take,
} from 'lodash';
import Fuse from 'fuse.js';
import PQueue from 'p-queue';
@ -194,8 +195,14 @@ const fuse = new Fuse(data, {
keys: ['name', 'short_name', 'short_names'],
});
export function search(query: string) {
return fuse.search(query.substr(0, 32));
export function search(query: string, count: number = 0) {
const results = fuse.search(query.substr(0, 32));
if (count) {
return take(results, count);
}
return results;
}
const shortNames = new Set([

View File

@ -4,6 +4,7 @@ import { noop } from 'lodash';
import { Manager, Popper, Reference } from 'react-popper';
import { createPortal } from 'react-dom';
import { StickerPicker } from './StickerPicker';
import { countStickers } from './lib';
import { StickerPackType, StickerType } from '../../state/ducks/stickers';
import { LocalizerType } from '../../types/Util';
@ -150,12 +151,14 @@ export const StickerButton = React.memo(
[installedPack, clearInstalledStickerPack]
);
const totalPacks =
knownPacks.length +
blessedPacks.length +
installedPacks.length +
receivedPacks.length;
if (totalPacks === 0) {
if (
countStickers({
knownPacks,
blessedPacks,
installedPacks,
receivedPacks,
}) === 0
) {
return null;
}

View File

@ -0,0 +1,17 @@
import { StickerPackType } from '../../state/ducks/stickers';
// This function exists to force stickers to be counted consistently wherever
// they are counted (TypeScript ensures that all data is named and provided)
export function countStickers(o: {
knownPacks: ReadonlyArray<StickerPackType>;
blessedPacks: ReadonlyArray<StickerPackType>;
installedPacks: ReadonlyArray<StickerPackType>;
receivedPacks: ReadonlyArray<StickerPackType>;
}) {
return (
o.knownPacks.length +
o.blessedPacks.length +
o.installedPacks.length +
o.receivedPacks.length
);
}

View File

@ -1,4 +1,5 @@
import { take, uniq } from 'lodash';
import { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
import { updateEmojiUsage } from '../../../js/modules/data';
// State
@ -27,7 +28,7 @@ export const actions = {
useEmoji,
};
function useEmoji(shortName: string): UseEmojiAction {
function useEmoji({ shortName }: EmojiPickDataType): UseEmojiAction {
return {
type: 'emojis/USE_EMOJI',
payload: doUseEmoji(shortName),

View File

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

View File

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

View File

@ -1,9 +1,11 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { get } from 'lodash';
import { mapDispatchToProps } from '../actions';
import { StickerButton } from '../../components/stickers/StickerButton';
import { CompositionArea } from '../../components/CompositionArea';
import { StateType } from '../reducer';
import { isShortName } from '../../components/emoji/lib';
import { getIntl } from '../selectors/user';
import {
getBlessedStickerPacks,
@ -14,6 +16,11 @@ import {
getRecentStickers,
} from '../selectors/stickers';
const selectRecentEmojis = createSelector(
({ emojis }: StateType) => emojis.recents,
recents => recents.filter(isShortName)
);
const mapStateToProps = (state: StateType) => {
const receivedPacks = getReceivedStickerPacks(state);
const installedPacks = getInstalledStickerPacks(state);
@ -31,7 +38,15 @@ const mapStateToProps = (state: StateType) => {
get(state.items, ['showStickerPickerHint'], false) &&
receivedPacks.length > 0;
const recentEmojis = selectRecentEmojis(state);
return {
// Base
i18n: getIntl(state),
// Emojis
recentEmojis,
skinTone: get(state, ['items', 'skinTone'], 0),
// Stickers
receivedPacks,
installedPack,
blessedPacks,
@ -40,16 +55,19 @@ const mapStateToProps = (state: StateType) => {
recentStickers,
showIntroduction,
showPickerHint,
i18n: getIntl(state),
};
};
const smart = connect(mapStateToProps, {
const dispatchPropsMap = {
...mapDispatchToProps,
onSetSkinTone: (tone: number) => mapDispatchToProps.putItem('skinTone', tone),
clearShowIntroduction: () =>
mapDispatchToProps.removeItem('showStickersIntroduction'),
clearShowPickerHint: () =>
mapDispatchToProps.removeItem('showStickerPickerHint'),
});
onPickEmoji: mapDispatchToProps.useEmoji,
};
export const SmartStickerButton = smart(StickerButton);
const smart = connect(mapStateToProps, dispatchPropsMap);
export const SmartCompositionArea = smart(CompositionArea);

View File

@ -1,58 +0,0 @@
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 { isShortName } from '../../components/emoji/lib';
import { getIntl } from '../selectors/user';
const selectRecentEmojis = createSelector(
({ emojis }: StateType) => emojis.recents,
recents => recents.filter(isShortName)
);
const mapStateToProps = (state: StateType) => {
return {
i18n: getIntl(state),
recentEmojis: selectRecentEmojis(state),
skinTone: get(state, ['items', 'skinTone'], 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

@ -477,7 +477,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " let $el = this.$(`#${id}`);",
"lineNumber": 33,
"lineNumber": 34,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
@ -486,7 +486,7 @@
"rule": "jQuery-prependTo(",
"path": "js/views/inbox_view.js",
"line": " $el.prependTo(this.el);",
"lineNumber": 42,
"lineNumber": 43,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
@ -495,7 +495,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.message').text(message);",
"lineNumber": 56,
"lineNumber": 61,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
@ -504,7 +504,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " el: this.$('.conversation-stack'),",
"lineNumber": 73,
"lineNumber": 78,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T21:59:32.770Z",
"reasonDetail": "Protected from arbitrary input"
@ -513,7 +513,7 @@
"rule": "jQuery-prependTo(",
"path": "js/views/inbox_view.js",
"line": " this.appLoadingScreen.$el.prependTo(this.el);",
"lineNumber": 80,
"lineNumber": 85,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
@ -522,7 +522,7 @@
"rule": "jQuery-append(",
"path": "js/views/inbox_view.js",
"line": " .append(this.networkStatusView.render().el);",
"lineNumber": 95,
"lineNumber": 100,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
@ -531,7 +531,7 @@
"rule": "jQuery-prependTo(",
"path": "js/views/inbox_view.js",
"line": " banner.$el.prependTo(this.$el);",
"lineNumber": 99,
"lineNumber": 104,
"reasonCategory": "usageTrusted",
"updated": "2018-09-19T18:13:29.628Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
@ -540,7 +540,7 @@
"rule": "jQuery-appendTo(",
"path": "js/views/inbox_view.js",
"line": " toast.$el.appendTo(this.$el);",
"lineNumber": 105,
"lineNumber": 110,
"reasonCategory": "usageTrusted",
"updated": "2019-05-10T00:25:51.515Z",
"reasonDetail": "Interacting with already-existing DOM nodes"
@ -549,7 +549,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
"lineNumber": 125,
"lineNumber": 130,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -558,7 +558,7 @@
"rule": "jQuery-append(",
"path": "js/views/inbox_view.js",
"line": " this.$('.left-pane-placeholder').append(this.leftPaneView.el);",
"lineNumber": 125,
"lineNumber": 130,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -567,7 +567,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " if (e && this.$(e.target).closest('.placeholder').length) {",
"lineNumber": 166,
"lineNumber": 171,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -576,7 +576,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('#header, .gutter').addClass('inactive');",
"lineNumber": 170,
"lineNumber": 175,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -585,7 +585,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.conversation-stack').addClass('inactive');",
"lineNumber": 174,
"lineNumber": 179,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -594,7 +594,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.conversation:first .menu').trigger('close');",
"lineNumber": 176,
"lineNumber": 181,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -603,7 +603,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " if (e && this.$(e.target).closest('.capture-audio').length > 0) {",
"lineNumber": 196,
"lineNumber": 201,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -612,7 +612,7 @@
"rule": "jQuery-$(",
"path": "js/views/inbox_view.js",
"line": " this.$('.conversation:first .recorder').trigger('close');",
"lineNumber": 199,
"lineNumber": 204,
"reasonCategory": "usageTrusted",
"updated": "2019-03-08T23:49:08.796Z",
"reasonDetail": "Protected from arbitrary input"
@ -2642,77 +2642,6 @@
"reasonCategory": "falseMatch",
"updated": "2019-04-26T19:26:59.689Z"
},
{
"rule": "fbjs-createNodesFromMarkup",
"path": "node_modules/create-react-context/node_modules/fbjs/lib/createNodesFromMarkup.js",
"line": "function createNodesFromMarkup(markup, handleScript) {",
"lineNumber": 51,
"reasonCategory": "falseMatch",
"updated": "2019-04-26T19:18:14.550Z"
},
{
"rule": "fbjs-createNodesFromMarkup",
"path": "node_modules/create-react-context/node_modules/fbjs/lib/createNodesFromMarkup.js",
"line": " !!!dummyNode ? process.env.NODE_ENV !== 'production' ? invariant(false, 'createNodesFromMarkup dummy not initialized') : invariant(false) : void 0;",
"lineNumber": 53,
"reasonCategory": "falseMatch",
"updated": "2019-04-26T19:18:14.550Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/create-react-context/node_modules/fbjs/lib/createNodesFromMarkup.js",
"line": " node.innerHTML = wrap[1] + markup + wrap[2];",
"lineNumber": 58,
"reasonCategory": "falseMatch",
"updated": "2019-04-26T19:18:14.550Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/create-react-context/node_modules/fbjs/lib/createNodesFromMarkup.js",
"line": " node.innerHTML = markup;",
"lineNumber": 65,
"reasonCategory": "falseMatch",
"updated": "2019-04-26T19:18:14.550Z"
},
{
"rule": "fbjs-createNodesFromMarkup",
"path": "node_modules/create-react-context/node_modules/fbjs/lib/createNodesFromMarkup.js",
"line": " !handleScript ? process.env.NODE_ENV !== 'production' ? invariant(false, 'createNodesFromMarkup(...): Unexpected <script> element rendered.') : invariant(false) : void 0;",
"lineNumber": 70,
"reasonCategory": "falseMatch",
"updated": "2019-04-26T19:18:14.550Z"
},
{
"rule": "fbjs-createNodesFromMarkup",
"path": "node_modules/create-react-context/node_modules/fbjs/lib/createNodesFromMarkup.js",
"line": "module.exports = createNodesFromMarkup;",
"lineNumber": 81,
"reasonCategory": "falseMatch",
"updated": "2019-04-26T19:18:14.550Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/create-react-context/node_modules/fbjs/lib/getMarkupWrap.js",
"line": " * Some browsers cannot use `innerHTML` to render certain elements standalone,",
"lineNumber": 23,
"reasonCategory": "falseMatch",
"updated": "2019-04-26T19:18:14.550Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/create-react-context/node_modules/fbjs/lib/getMarkupWrap.js",
"line": " dummyNode.innerHTML = '<link />';",
"lineNumber": 83,
"reasonCategory": "falseMatch"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/create-react-context/node_modules/fbjs/lib/getMarkupWrap.js",
"line": " dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';",
"lineNumber": 85,
"reasonCategory": "falseMatch",
"updated": "2019-04-26T19:18:14.550Z"
},
{
"rule": "jQuery-prepend(",
"path": "node_modules/css/node_modules/source-map/lib/source-map/source-node.js",
@ -2919,6 +2848,230 @@
"reasonCategory": "falseMatch",
"updated": "2018-09-19T21:59:32.770Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/dist/Draft.js",
"line": "\t var blockNode = ReactDOM.findDOMNode(this);",
"lineNumber": 3773,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/draft-js/dist/Draft.js",
"line": "\t doc.documentElement.innerHTML = html;",
"lineNumber": 5535,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/dist/Draft.js",
"line": "\t var editorNode = ReactDOM.findDOMNode(_this.editor);",
"lineNumber": 7047,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/dist/Draft.js",
"line": "\t var editorNode = ReactDOM.findDOMNode(_this.editor);",
"lineNumber": 7081,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/draft-js/dist/Draft.js",
"line": "\t * Resetting innerHTML will move focus to the beginning of the editor,",
"lineNumber": 7570,
"reasonCategory": "falseMatch",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/dist/Draft.js",
"line": "\t var node = ReactDOM.findDOMNode(this);",
"lineNumber": 8140,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/dist/Draft.js",
"line": "\t var leafNode = ReactDOM.findDOMNode(this.leaf);",
"lineNumber": 8168,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/dist/Draft.js",
"line": "\t var node = ReactDOM.findDOMNode(this);",
"lineNumber": 8406,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/dist/Draft.js",
"line": "\t var editorNode = ReactDOM.findDOMNode(editor.editorContainer);",
"lineNumber": 10608,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "DOM-outerHTML",
"path": "node_modules/draft-js/dist/Draft.js",
"line": "\t return anonymized.outerHTML;",
"lineNumber": 12689,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/dist/Draft.min.js",
"lineNumber": 16,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/draft-js/dist/Draft.min.js",
"lineNumber": 16,
"reasonCategory": "falseMatch",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/draft-js/dist/Draft.min.js",
"lineNumber": 17,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/dist/Draft.min.js",
"lineNumber": 17,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/dist/Draft.min.js",
"lineNumber": 18,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "DOM-outerHTML",
"path": "node_modules/draft-js/dist/Draft.min.js",
"lineNumber": 19,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/lib/DraftEditor.react.js",
"line": " var editorNode = ReactDOM.findDOMNode(_this.editor);",
"lineNumber": 82,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/lib/DraftEditor.react.js",
"line": " var editorNode = ReactDOM.findDOMNode(_this.editor);",
"lineNumber": 116,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/lib/DraftEditorBlock.react.js",
"line": " var blockNode = ReactDOM.findDOMNode(this);",
"lineNumber": 92,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/lib/DraftEditorBlockNode.react.js",
"line": " var blockNode = ReactDOM.findDOMNode(this);",
"lineNumber": 215,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/draft-js/lib/DraftEditorCompositionHandler.js",
"line": " * Resetting innerHTML will move focus to the beginning of the editor,",
"lineNumber": 128,
"reasonCategory": "falseMatch",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/lib/DraftEditorLeaf.react.js",
"line": " var node = ReactDOM.findDOMNode(this);",
"lineNumber": 72,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/lib/DraftEditorLeaf.react.js",
"line": " var leafNode = ReactDOM.findDOMNode(this.leaf);",
"lineNumber": 100,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/lib/DraftEditorTextNode.react.js",
"line": " var node = ReactDOM.findDOMNode(this);",
"lineNumber": 84,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "React-findDOMNode",
"path": "node_modules/draft-js/lib/editOnSelect.js",
"line": " var editorNode = ReactDOM.findDOMNode(editor.editorContainer);",
"lineNumber": 28,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/draft-js/lib/getSafeBodyFromHTML.js",
"line": " doc.documentElement.innerHTML = html;",
"lineNumber": 33,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "DOM-outerHTML",
"path": "node_modules/draft-js/lib/setDraftEditorSelection.js",
"line": " return anonymized.outerHTML;",
"lineNumber": 33,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/draft-js/node_modules/immutable/dist/immutable.min.js",
"lineNumber": 11,
"reasonCategory": "falseMatch",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/draft-js/node_modules/immutable/dist/immutable.min.js",
"lineNumber": 23,
"reasonCategory": "falseMatch",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "jQuery-wrap(",
"path": "node_modules/duplexer3/index.js",
@ -3390,6 +3543,27 @@
"reasonCategory": "falseMatch",
"updated": "2018-11-27T18:02:26.186Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/immutable/dist/immutable.min.js",
"lineNumber": 9,
"reasonCategory": "falseMatch",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/immutable/dist/immutable.min.js",
"lineNumber": 21,
"reasonCategory": "falseMatch",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/immutable/dist/immutable.min.js",
"lineNumber": 34,
"reasonCategory": "falseMatch",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "jQuery-$(",
"path": "node_modules/intl-tel-input/build/js/intlTelInput.js",
@ -5003,6 +5177,78 @@
"reasonCategory": "falseMatch",
"updated": "2018-09-19T18:13:29.628Z"
},
{
"rule": "fbjs-createNodesFromMarkup",
"path": "node_modules/prop-types/node_modules/fbjs/lib/createNodesFromMarkup.js",
"line": "function createNodesFromMarkup(markup, handleScript) {",
"lineNumber": 51,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "fbjs-createNodesFromMarkup",
"path": "node_modules/prop-types/node_modules/fbjs/lib/createNodesFromMarkup.js",
"line": " !!!dummyNode ? process.env.NODE_ENV !== 'production' ? invariant(false, 'createNodesFromMarkup dummy not initialized') : invariant(false) : void 0;",
"lineNumber": 53,
"reasonCategory": "falseMatch",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/prop-types/node_modules/fbjs/lib/createNodesFromMarkup.js",
"line": " node.innerHTML = wrap[1] + markup + wrap[2];",
"lineNumber": 58,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/prop-types/node_modules/fbjs/lib/createNodesFromMarkup.js",
"line": " node.innerHTML = markup;",
"lineNumber": 65,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "fbjs-createNodesFromMarkup",
"path": "node_modules/prop-types/node_modules/fbjs/lib/createNodesFromMarkup.js",
"line": " !handleScript ? process.env.NODE_ENV !== 'production' ? invariant(false, 'createNodesFromMarkup(...): Unexpected <script> element rendered.') : invariant(false) : void 0;",
"lineNumber": 70,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "fbjs-createNodesFromMarkup",
"path": "node_modules/prop-types/node_modules/fbjs/lib/createNodesFromMarkup.js",
"line": "module.exports = createNodesFromMarkup;",
"lineNumber": 81,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/prop-types/node_modules/fbjs/lib/getMarkupWrap.js",
"line": " * Some browsers cannot use `innerHTML` to render certain elements standalone,",
"lineNumber": 23,
"reasonCategory": "falseMatch",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/prop-types/node_modules/fbjs/lib/getMarkupWrap.js",
"line": " dummyNode.innerHTML = '<link />';",
"lineNumber": 83,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "DOM-innerHTML",
"path": "node_modules/prop-types/node_modules/fbjs/lib/getMarkupWrap.js",
"line": " dummyNode.innerHTML = '<' + nodeName + '></' + nodeName + '>';",
"lineNumber": 85,
"reasonCategory": "usageTrusted",
"updated": "2019-06-20T20:21:33.456Z"
},
{
"rule": "eval",
"path": "node_modules/protobufjs/dist/light/protobuf.js",

View File

@ -14,6 +14,13 @@
dependencies:
regenerator-runtime "^0.12.0"
"@babel/runtime@^7.2.0":
version "7.4.5"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
dependencies:
regenerator-runtime "^0.13.2"
"@journeyapps/sqlcipher@https://github.com/scottnonnenberg-signal/node-sqlcipher.git#2e28733b61640556b0272a3bfc78b0357daf71e6":
version "3.2.1"
resolved "https://github.com/scottnonnenberg-signal/node-sqlcipher.git#2e28733b61640556b0272a3bfc78b0357daf71e6"
@ -94,6 +101,14 @@
resolved "https://registry.yarnpkg.com/@types/config/-/config-0.0.34.tgz#123f91bdb5afdd702294b9de9ca04d9ea11137b0"
integrity sha512-jWi9DXx77hnzN4kHCNEvP/kab+nchRLTg9yjXYxjTcMBkuc5iBb3QuwJ4sPrb+nzy1GQjrfyfMqZOdR4i7opRQ==
"@types/draft-js@0.10.32":
version "0.10.32"
resolved "https://registry.yarnpkg.com/@types/draft-js/-/draft-js-0.10.32.tgz#cbfed40c500d9bb1e486813dde73125291e7517d"
integrity sha512-x63qkMUVpK8lAdxYQFk54F92F1mcG202qs4t0I+UqsZyCpkRRJlMQ+1v3QBbPcckVlmmdSg7m2PqoAbeXNjCaA==
dependencies:
"@types/react" "*"
immutable "^3.8.1"
"@types/events@*":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
@ -219,6 +234,13 @@
dependencies:
"@types/react" "*"
"@types/react-measure@2.0.5":
version "2.0.5"
resolved "https://registry.yarnpkg.com/@types/react-measure/-/react-measure-2.0.5.tgz#c1d304e3cab3a1c393342bf377b040628e6c29a8"
integrity sha512-T1Bpt8FlWbDhoInUaNrjTOiVRpRJmrRcqhFJxLGBq1VjaqBLHCvUPapgdKMWEIX4Oqsa1SSKjtNkNJGy6WAAZg==
dependencies:
"@types/react" "*"
"@types/react-redux@7.0.1":
version "7.0.1"
resolved "https://registry.yarnpkg.com/@types/react-redux/-/react-redux-7.0.1.tgz#9dd2503be7a9861c5a092bf1c5050b7ade4dc62e"
@ -2577,6 +2599,15 @@ dotenv@^6.2.0:
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-6.2.0.tgz#941c0410535d942c8becf28d3f357dbd9d476064"
integrity sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==
draft-js@0.10.5:
version "0.10.5"
resolved "https://registry.yarnpkg.com/draft-js/-/draft-js-0.10.5.tgz#bfa9beb018fe0533dbb08d6675c371a6b08fa742"
integrity sha512-LE6jSCV9nkPhfVX2ggcRLA4FKs6zWq9ceuO/88BpXdNCS7mjRTgs0NsV6piUCJX9YxMsB9An33wnkMmU2sD2Zg==
dependencies:
fbjs "^0.8.15"
immutable "~3.7.4"
object-assign "^4.1.0"
dtrace-provider@~0.8:
version "0.8.7"
resolved "https://registry.yarnpkg.com/dtrace-provider/-/dtrace-provider-0.8.7.tgz#dc939b4d3e0620cfe0c1cd803d0d2d7ed04ffd04"
@ -2779,6 +2810,11 @@ emoji-js@3.4.0:
dependencies:
emoji-datasource "4.0.0"
emoji-regex@8.0.0:
version "8.0.0"
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
"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"
@ -3359,7 +3395,7 @@ faye-websocket@~0.11.0:
dependencies:
websocket-driver ">=0.5.1"
fbjs@^0.8.0:
fbjs@^0.8.0, fbjs@^0.8.15:
version "0.8.17"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
@ -3733,7 +3769,7 @@ 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:
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==
@ -3770,6 +3806,11 @@ get-func-name@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41"
get-node-dimensions@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/get-node-dimensions/-/get-node-dimensions-1.2.1.tgz#fb7b4bb57060fb4247dd51c9d690dfbec56b0823"
integrity sha512-2MSPMu7S1iOTL+BOa6K1S62hB2zUAYNF/lV0gSVlOaacd087lc6nR1H1r0e3B1CerTo+RceOmi1iJW+vp21xcQ==
get-own-enumerable-property-symbols@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz#5c4ad87f2834c4b9b4e84549dc1e0650fb38c24b"
@ -4546,6 +4587,16 @@ immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
immutable@^3.8.1:
version "3.8.2"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3"
integrity sha1-wkOZUUVbs5kT2vKBN28VMOEErfM=
immutable@~3.7.4:
version "3.7.6"
resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.7.6.tgz#13b4d3cb12befa15482a26fe1b2ebae640071e4b"
integrity sha1-E7TTyxK++hVIKib+Gy665kAHHks=
import-lazy@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43"
@ -7758,7 +7809,17 @@ react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-popper@^1.3.3:
react-measure@2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/react-measure/-/react-measure-2.3.0.tgz#75835d39abec9ae13517f35a819c160997a7a44e"
integrity sha512-dwAvmiOeblj5Dvpnk8Jm7Q8B4THF/f1l1HtKVi0XDecsG6LXwGvzV5R1H32kq3TW6RW64OAf5aoQxpIgLa4z8A==
dependencies:
"@babel/runtime" "^7.2.0"
get-node-dimensions "^1.2.1"
prop-types "^15.6.2"
resize-observer-polyfill "^1.5.0"
react-popper@1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.3.tgz#2c6cef7515a991256b4f0536cd4bdcb58a7b6af6"
integrity sha512-ynMZBPkXONPc5K4P5yFWgZx5JGAUIP3pGGLNs58cfAPgK67olx7fmLp+AdpZ0+GoQ+ieFDa/z4cdV6u7sioH6w==
@ -8083,6 +8144,11 @@ regenerator-runtime@^0.12.0:
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
regenerator-runtime@^0.13.2:
version "0.13.2"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447"
integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==
regex-cache@^0.4.2:
version "0.4.4"
resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd"
@ -8336,6 +8402,11 @@ reselect@4.0.0:
resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7"
integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA==
resize-observer-polyfill@^1.5.0:
version "1.5.1"
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-cwd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"