From 7b645011c25bda9d4cc1111b253666a661774652 Mon Sep 17 00:00:00 2001 From: Ken Powers Date: Thu, 27 Jun 2019 16:35:21 -0400 Subject: [PATCH] New composition area with emoji typeahead --- _locales/ar/messages.json | 10 - _locales/bg/messages.json | 10 - _locales/ca/messages.json | 10 - _locales/cs/messages.json | 10 - _locales/da/messages.json | 10 - _locales/de/messages.json | 10 - _locales/el/messages.json | 10 - _locales/en/messages.json | 10 - _locales/eo/messages.json | 10 - _locales/es/messages.json | 10 - _locales/es_419/messages.json | 10 - _locales/et/messages.json | 10 - _locales/fa/messages.json | 10 - _locales/fi/messages.json | 10 - _locales/fr/messages.json | 10 - _locales/he/messages.json | 10 - _locales/hi/messages.json | 10 - _locales/hr/messages.json | 10 - _locales/hu/messages.json | 10 - _locales/id/messages.json | 10 - _locales/it/messages.json | 10 - _locales/ja/messages.json | 10 - _locales/km/messages.json | 10 - _locales/kn/messages.json | 10 - _locales/ko/messages.json | 10 - _locales/lt/messages.json | 10 - _locales/mk/messages.json | 10 - _locales/nb/messages.json | 10 - _locales/nl/messages.json | 10 - _locales/nn/messages.json | 10 - _locales/no/messages.json | 10 - _locales/pl/messages.json | 10 - _locales/pt_BR/messages.json | 10 - _locales/pt_PT/messages.json | 10 - _locales/ro/messages.json | 10 - _locales/ru/messages.json | 10 - _locales/sk/messages.json | 10 - _locales/sl/messages.json | 10 - _locales/sq/messages.json | 10 - _locales/sr/messages.json | 10 - _locales/sv/messages.json | 10 - _locales/th/messages.json | 10 - _locales/tr/messages.json | 10 - _locales/uk/messages.json | 10 - _locales/vi/messages.json | 10 - _locales/zh_CN/messages.json | 10 - _locales/zh_TW/messages.json | 10 - background.html | 5 +- bower.json | 4 - components/autosize/dist/autosize.js | 292 -------- images/emoji-object-filled-20.svg | 8 +- images/emoji-object-outline-20.svg | 9 +- images/emoji-symbol-filled-20.svg | 9 +- images/emoji-symbol-outline-20.svg | 11 +- js/modules/signal.js | 10 +- js/views/conversation_view.js | 292 ++------ js/views/inbox_view.js | 5 + package.json | 9 +- styleguide.config.js | 5 + stylesheets/_modules.scss | 177 ++++- ts/components/CompositionArea.md | 29 + ts/components/CompositionArea.tsx | 182 +++++ ts/components/CompositionInput.md | 12 + ts/components/CompositionInput.tsx | 693 ++++++++++++++++++ ts/components/ContactListItem.tsx | 4 +- ts/components/ConversationListItem.tsx | 1 - ts/components/MessageBodyHighlight.tsx | 2 - ts/components/MessageSearchResult.tsx | 2 - ts/components/conversation/ContactName.md | 9 +- ts/components/conversation/ContactName.tsx | 9 +- .../conversation/ConversationHeader.tsx | 4 +- ts/components/conversation/Emojify.md | 26 +- ts/components/conversation/Emojify.tsx | 17 +- .../conversation/GroupNotification.tsx | 1 - ts/components/conversation/Message.tsx | 2 - ts/components/conversation/MessageBody.tsx | 2 - ts/components/conversation/MessageDetail.tsx | 1 - ts/components/conversation/Quote.tsx | 1 - .../conversation/SafetyNumberNotification.tsx | 1 - .../conversation/TimerNotification.tsx | 1 - .../conversation/UnsupportedMessage.tsx | 1 - .../conversation/VerificationNotification.tsx | 1 - ts/components/emoji/Emoji.tsx | 40 +- ts/components/emoji/EmojiButton.tsx | 21 +- ts/components/emoji/EmojiPicker.tsx | 12 +- ts/components/emoji/lib.ts | 11 +- ts/components/stickers/StickerButton.tsx | 15 +- ts/components/stickers/lib.ts | 17 + ts/state/ducks/emojis.ts | 3 +- ...jiButton.tsx => createCompositionArea.tsx} | 8 +- ts/state/roots/createStickerButton.tsx | 16 - ...{StickerButton.tsx => CompositionArea.tsx} | 28 +- ts/state/smart/EmojiButton.tsx | 58 -- ts/util/lint/exceptions.json | 420 ++++++++--- yarn.lock | 77 +- 95 files changed, 1740 insertions(+), 1293 deletions(-) delete mode 100644 components/autosize/dist/autosize.js mode change 100755 => 100644 images/emoji-object-filled-20.svg mode change 100755 => 100644 images/emoji-object-outline-20.svg mode change 100755 => 100644 images/emoji-symbol-filled-20.svg mode change 100755 => 100644 images/emoji-symbol-outline-20.svg create mode 100644 ts/components/CompositionArea.md create mode 100644 ts/components/CompositionArea.tsx create mode 100644 ts/components/CompositionInput.md create mode 100644 ts/components/CompositionInput.tsx create mode 100644 ts/components/stickers/lib.ts rename ts/state/roots/{createEmojiButton.tsx => createCompositionArea.tsx} (55%) delete mode 100644 ts/state/roots/createStickerButton.tsx rename ts/state/smart/{StickerButton.tsx => CompositionArea.tsx} (65%) delete mode 100644 ts/state/smart/EmojiButton.tsx diff --git a/_locales/ar/messages.json b/_locales/ar/messages.json index 1d0a06511..4340683a5 100644 --- a/_locales/ar/messages.json +++ b/_locales/ar/messages.json @@ -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" diff --git a/_locales/bg/messages.json b/_locales/bg/messages.json index afc97a461..13871fe5d 100644 --- a/_locales/bg/messages.json +++ b/_locales/bg/messages.json @@ -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" diff --git a/_locales/ca/messages.json b/_locales/ca/messages.json index 8afa9689c..8fb75ee18 100644 --- a/_locales/ca/messages.json +++ b/_locales/ca/messages.json @@ -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" diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index 991d609ab..7fb481f1c 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -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" diff --git a/_locales/da/messages.json b/_locales/da/messages.json index e7dec3fdd..374147983 100644 --- a/_locales/da/messages.json +++ b/_locales/da/messages.json @@ -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" diff --git a/_locales/de/messages.json b/_locales/de/messages.json index bf173d292..195235282 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -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" diff --git a/_locales/el/messages.json b/_locales/el/messages.json index a9cd493dd..72fbb23b5 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -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" diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 63465d743..f4e3dcbbb 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" diff --git a/_locales/eo/messages.json b/_locales/eo/messages.json index 98904a09a..de9cd635d 100644 --- a/_locales/eo/messages.json +++ b/_locales/eo/messages.json @@ -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" diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 623ed7c13..5dc4cf7b8 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -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" diff --git a/_locales/es_419/messages.json b/_locales/es_419/messages.json index 92243e7d5..0224997cd 100644 --- a/_locales/es_419/messages.json +++ b/_locales/es_419/messages.json @@ -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" diff --git a/_locales/et/messages.json b/_locales/et/messages.json index 4633d5074..7791b10dd 100644 --- a/_locales/et/messages.json +++ b/_locales/et/messages.json @@ -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" diff --git a/_locales/fa/messages.json b/_locales/fa/messages.json index 5b5469999..bd1b5ff72 100644 --- a/_locales/fa/messages.json +++ b/_locales/fa/messages.json @@ -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" diff --git a/_locales/fi/messages.json b/_locales/fi/messages.json index f88014578..0f76e3708 100644 --- a/_locales/fi/messages.json +++ b/_locales/fi/messages.json @@ -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" diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index 07f69fefb..cf218d4fa 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -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" diff --git a/_locales/he/messages.json b/_locales/he/messages.json index 6f204e5fd..a1c64d951 100644 --- a/_locales/he/messages.json +++ b/_locales/he/messages.json @@ -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" diff --git a/_locales/hi/messages.json b/_locales/hi/messages.json index b6d4181fc..b17ffea5b 100644 --- a/_locales/hi/messages.json +++ b/_locales/hi/messages.json @@ -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" diff --git a/_locales/hr/messages.json b/_locales/hr/messages.json index e686604ca..4026716cb 100644 --- a/_locales/hr/messages.json +++ b/_locales/hr/messages.json @@ -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" diff --git a/_locales/hu/messages.json b/_locales/hu/messages.json index c3f6dde18..3b4a745bf 100644 --- a/_locales/hu/messages.json +++ b/_locales/hu/messages.json @@ -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" diff --git a/_locales/id/messages.json b/_locales/id/messages.json index 8444f4694..5b880da9f 100644 --- a/_locales/id/messages.json +++ b/_locales/id/messages.json @@ -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" diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 10ebb91f9..4ccbb8968 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -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" diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 4428ce739..9bb9c5d1a 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -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" diff --git a/_locales/km/messages.json b/_locales/km/messages.json index 3063cb176..64b95a3f8 100644 --- a/_locales/km/messages.json +++ b/_locales/km/messages.json @@ -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" diff --git a/_locales/kn/messages.json b/_locales/kn/messages.json index a07a7b8ac..019ed11ed 100644 --- a/_locales/kn/messages.json +++ b/_locales/kn/messages.json @@ -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" diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json index 8d0989332..3ebc42cd4 100644 --- a/_locales/ko/messages.json +++ b/_locales/ko/messages.json @@ -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" diff --git a/_locales/lt/messages.json b/_locales/lt/messages.json index 0b3fa133b..199c2c8a4 100644 --- a/_locales/lt/messages.json +++ b/_locales/lt/messages.json @@ -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" diff --git a/_locales/mk/messages.json b/_locales/mk/messages.json index 5e3aa0c0f..c41bab92d 100644 --- a/_locales/mk/messages.json +++ b/_locales/mk/messages.json @@ -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" diff --git a/_locales/nb/messages.json b/_locales/nb/messages.json index c65db64e5..e46e399c7 100644 --- a/_locales/nb/messages.json +++ b/_locales/nb/messages.json @@ -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" diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index fcf1295a7..5ec677ca8 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -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" diff --git a/_locales/nn/messages.json b/_locales/nn/messages.json index ed41ca415..70cd4fd86 100644 --- a/_locales/nn/messages.json +++ b/_locales/nn/messages.json @@ -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" diff --git a/_locales/no/messages.json b/_locales/no/messages.json index c6521e715..90b4d78cb 100644 --- a/_locales/no/messages.json +++ b/_locales/no/messages.json @@ -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" diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index 891735619..ecafd41c5 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -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" diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json index ad32ff6bf..ff9b5b822 100644 --- a/_locales/pt_BR/messages.json +++ b/_locales/pt_BR/messages.json @@ -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" diff --git a/_locales/pt_PT/messages.json b/_locales/pt_PT/messages.json index 7901a4f77..06cabe7f2 100644 --- a/_locales/pt_PT/messages.json +++ b/_locales/pt_PT/messages.json @@ -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" diff --git a/_locales/ro/messages.json b/_locales/ro/messages.json index abd876712..9239de4a7 100644 --- a/_locales/ro/messages.json +++ b/_locales/ro/messages.json @@ -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" diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index fe2dfc840..2909ad437 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -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" diff --git a/_locales/sk/messages.json b/_locales/sk/messages.json index d225b13f0..39b93f7bb 100644 --- a/_locales/sk/messages.json +++ b/_locales/sk/messages.json @@ -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" diff --git a/_locales/sl/messages.json b/_locales/sl/messages.json index 4ab526c6b..5b198cb92 100644 --- a/_locales/sl/messages.json +++ b/_locales/sl/messages.json @@ -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" diff --git a/_locales/sq/messages.json b/_locales/sq/messages.json index 3c6966608..1e47563a6 100644 --- a/_locales/sq/messages.json +++ b/_locales/sq/messages.json @@ -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" diff --git a/_locales/sr/messages.json b/_locales/sr/messages.json index 0f2584ad7..bccd0c5e2 100644 --- a/_locales/sr/messages.json +++ b/_locales/sr/messages.json @@ -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" diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 915a9bb44..583781d51 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -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" diff --git a/_locales/th/messages.json b/_locales/th/messages.json index 504363ad9..f27a8b4df 100644 --- a/_locales/th/messages.json +++ b/_locales/th/messages.json @@ -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" diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json index 24786d333..e49db76cb 100644 --- a/_locales/tr/messages.json +++ b/_locales/tr/messages.json @@ -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" diff --git a/_locales/uk/messages.json b/_locales/uk/messages.json index 42a4b58e0..0da862ab8 100644 --- a/_locales/uk/messages.json +++ b/_locales/uk/messages.json @@ -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" diff --git a/_locales/vi/messages.json b/_locales/vi/messages.json index 8d52fff3a..a540561f3 100644 --- a/_locales/vi/messages.json +++ b/_locales/vi/messages.json @@ -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" diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 9ad23fe2d..1b4e35812 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -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" diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index 9a675c1b3..a5c72827b 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -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" diff --git a/background.html b/background.html index bb235bd93..ada98bf3d 100644 --- a/background.html +++ b/background.html @@ -22,6 +22,7 @@ Signal + - - - +emoji-object-solid-20 \ No newline at end of file diff --git a/images/emoji-object-outline-20.svg b/images/emoji-object-outline-20.svg old mode 100755 new mode 100644 index c8edca645..22cd0b2f9 --- a/images/emoji-object-outline-20.svg +++ b/images/emoji-object-outline-20.svg @@ -1,8 +1 @@ - - - - - +emoji-object-outline-20 \ No newline at end of file diff --git a/images/emoji-symbol-filled-20.svg b/images/emoji-symbol-filled-20.svg old mode 100755 new mode 100644 index 849954e13..23c5f0434 --- a/images/emoji-symbol-filled-20.svg +++ b/images/emoji-symbol-filled-20.svg @@ -1,8 +1 @@ - - - - - +emoji-symbol-solid-20 \ No newline at end of file diff --git a/images/emoji-symbol-outline-20.svg b/images/emoji-symbol-outline-20.svg old mode 100755 new mode 100644 index a039e1602..f833efb41 --- a/images/emoji-symbol-outline-20.svg +++ b/images/emoji-symbol-outline-20.svg @@ -1,10 +1 @@ - - - - - +emoji-symbol-outline-20 \ No newline at end of file diff --git a/js/modules/signal.js b/js/modules/signal.js index 69410d1a2..2f206fdba 100644 --- a/js/modules/signal.js +++ b/js/modules/signal.js @@ -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, }; diff --git a/js/views/conversation_view.js b/js/views/conversation_view.js index 177cea9a4..ae93ef580 100644 --- a/js/views/conversation_view.js +++ b/js/views/conversation_view.js @@ -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' || diff --git a/js/views/inbox_view.js b/js/views/inbox_view.js index ffac5cff9..12ebb4857 100644 --- a/js/views/inbox_view.js +++ b/js/views/inbox_view.js @@ -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')); }, diff --git a/package.json b/package.json index 63b092ac9..242349002 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/styleguide.config.js b/styleguide.config.js index b0faae581..7185f36fc 100644 --- a/styleguide.config.js +++ b/styleguide.config.js @@ -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', + }, ], }, }, diff --git a/stylesheets/_modules.scss b/stylesheets/_modules.scss index 449599ec1..8adad359c 100644 --- a/stylesheets/_modules.scss +++ b/stylesheets/_modules.scss @@ -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 { diff --git a/ts/components/CompositionArea.md b/ts/components/CompositionArea.md new file mode 100644 index 000000000..fdd59ea57 --- /dev/null +++ b/ts/components/CompositionArea.md @@ -0,0 +1,29 @@ +#### Default + +```jsx + +
+ 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')} + /> +
+
+``` diff --git a/ts/components/CompositionArea.tsx b/ts/components/CompositionArea.tsx new file mode 100644 index 000000000..6b61e852a --- /dev/null +++ b/ts/components/CompositionArea.tsx @@ -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(null); + const inputApiRef = React.useRef(); + + 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 ( +
+
+ +
+
+ +
+ {withStickers ? ( +
+ +
+ ) : null} +
+ ); +}; diff --git a/ts/components/CompositionInput.md b/ts/components/CompositionInput.md new file mode 100644 index 000000000..705ee852c --- /dev/null +++ b/ts/components/CompositionInput.md @@ -0,0 +1,12 @@ +#### Default + +```jsx + +
+ console.log('onSubmit', s)} + /> +
+
+``` diff --git a/ts/components/CompositionInput.tsx b/ts/components/CompositionInput.tsx new file mode 100644 index 000000000..b5beb22c9 --- /dev/null +++ b/ts/components/CompositionInput.tsx @@ -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; + readonly inputApi?: React.MutableRefObject; + 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 ? ( + + {children} + + ) : ( + 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) => 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(''); + const [emojiResults, setEmojiResults] = React.useState>([]); + const [emojiResultsIndex, setEmojiResultsIndex] = React.useState(0); + const [editorWidth, setEditorWidth] = React.useState(0); + const [popperRoot, setPopperRoot] = React.useState( + null + ); + const dirtyRef = React.useRef(false); + const focusRef = React.useRef(false); + const editorStateRef = React.useRef(editorState); + const rootElRef = React.useRef(); + + // 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 ( + + + {({ ref: popperRef }) => ( + + {({ measureRef }) => ( +
+
+ +
+
+ )} +
+ )} +
+ {emojiResults.length > 0 && popperRoot + ? createPortal( + + {({ ref, style }) => ( +
+ {emojiResults.map((emoji, index) => ( + + ))} +
+ )} +
, + popperRoot + ) + : null} +
+ ); +}; diff --git a/ts/components/ContactListItem.tsx b/ts/components/ContactListItem.tsx index d53ce172b..68607de67 100644 --- a/ts/components/ContactListItem.tsx +++ b/ts/components/ContactListItem.tsx @@ -60,7 +60,7 @@ export class ContactListItem extends React.Component { const profileElement = !isMe && profileName && !name ? ( - ~ + ~ ) : null; @@ -79,7 +79,7 @@ export class ContactListItem extends React.Component { {this.renderAvatar()}
- {profileElement} + {profileElement}
{showVerified ? ( diff --git a/ts/components/ConversationListItem.tsx b/ts/components/ConversationListItem.tsx index 2e5ba3795..d27773cb3 100644 --- a/ts/components/ConversationListItem.tsx +++ b/ts/components/ConversationListItem.tsx @@ -111,7 +111,6 @@ export class ConversationListItem extends React.PureComponent { phoneNumber={phoneNumber} name={name} profileName={profileName} - i18n={i18n} /> )}
diff --git a/ts/components/MessageBodyHighlight.tsx b/ts/components/MessageBodyHighlight.tsx index 3cb2652b5..d28676c72 100644 --- a/ts/components/MessageBodyHighlight.tsx +++ b/ts/components/MessageBodyHighlight.tsx @@ -18,7 +18,6 @@ const renderNewLines: RenderTextCallbackType = ({ text, key }) => ( ); const renderEmoji = ({ - i18n, text, key, sizeClass, @@ -31,7 +30,6 @@ const renderEmoji = ({ renderNonEmoji: RenderTextCallbackType; }) => ( { 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 { phoneNumber={to.phoneNumber} name={to.name} profileName={to.profileName} - i18n={i18n} />
diff --git a/ts/components/conversation/ContactName.md b/ts/components/conversation/ContactName.md index a12091883..6d062440b 100644 --- a/ts/components/conversation/ContactName.md +++ b/ts/components/conversation/ContactName.md @@ -2,7 +2,6 @@ ```jsx + ``` #### No name, no profile ```jsx - + ``` diff --git a/ts/components/conversation/ContactName.tsx b/ts/components/conversation/ContactName.tsx index 3eda7d542..d3e8fe41b 100644 --- a/ts/components/conversation/ContactName.tsx +++ b/ts/components/conversation/ContactName.tsx @@ -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 { 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 ? ( - ~ + ~ ) : null; return ( - + {shouldShowProfile ? ' ' : null} {profileElement} diff --git a/ts/components/conversation/ConversationHeader.tsx b/ts/components/conversation/ConversationHeader.tsx index 315838939..9b75793d5 100644 --- a/ts/components/conversation/ConversationHeader.tsx +++ b/ts/components/conversation/ConversationHeader.tsx @@ -101,12 +101,12 @@ export class ConversationHeader extends React.Component { return (
- {name ? : null} + {name ? : null} {name && phoneNumber ? ' · ' : null} {phoneNumber ? phoneNumber : null}{' '} {profileName && !name ? ( - ~ + ~ ) : null} {isVerified ? ' · ' : null} diff --git a/ts/components/conversation/Emojify.md b/ts/components/conversation/Emojify.md index 386db385d..17e2c9e1a 100644 --- a/ts/components/conversation/Emojify.md +++ b/ts/components/conversation/Emojify.md @@ -1,53 +1,53 @@ ### All emoji ```jsx - + ``` ### With skin color modifier ```jsx - + ``` ### With `sizeClass` provided ```jsx - + ``` ```jsx - + ``` ```jsx - + ``` ```jsx - + ``` ```jsx - + ``` ### Starting and ending with emoji ```jsx - + ``` ### With emoji in the middle ```jsx - + ``` ### No emoji ```jsx - + ``` ### Providing custom non-link render function @@ -56,9 +56,5 @@ const renderNonEmoji = ({ text, key }) => ( This is my custom content ); -; +; ``` diff --git a/ts/components/conversation/Emojify.tsx b/ts/components/conversation/Emojify.tsx index c9727507f..61ffc1c80 100644 --- a/ts/components/conversation/Emojify.tsx +++ b/ts/components/conversation/Emojify.tsx @@ -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({ ); } @@ -62,7 +56,6 @@ interface Props { sizeClass?: SizeClassType; /** Allows you to customize now non-newlines are rendered. Simplest is just a . */ renderNonEmoji?: RenderTextCallbackType; - i18n: LocalizerType; } export class Emojify extends React.Component { @@ -71,7 +64,7 @@ export class Emojify extends React.Component { }; public render() { - const { text, sizeClass, renderNonEmoji, i18n } = this.props; + const { text, sizeClass, renderNonEmoji } = this.props; const results: Array = []; const regex = getRegex(); @@ -95,7 +88,7 @@ export class Emojify extends React.Component { 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); diff --git a/ts/components/conversation/GroupNotification.tsx b/ts/components/conversation/GroupNotification.tsx index a1bf84052..6d008d814 100644 --- a/ts/components/conversation/GroupNotification.tsx +++ b/ts/components/conversation/GroupNotification.tsx @@ -45,7 +45,6 @@ export class GroupNotification extends React.Component { className="module-group-notification__contact" > { collapseMetadata, conversationType, direction, - i18n, isSticker, isTapToView, isTapToViewExpired, @@ -361,7 +360,6 @@ export class Message extends React.PureComponent { name={authorName} profileName={authorProfileName} module={moduleName} - i18n={i18n} />
); diff --git a/ts/components/conversation/MessageBody.tsx b/ts/components/conversation/MessageBody.tsx index fa04036c9..1801ed9ed 100644 --- a/ts/components/conversation/MessageBody.tsx +++ b/ts/components/conversation/MessageBody.tsx @@ -23,7 +23,6 @@ const renderNewLines: RenderTextCallbackType = ({ }) => ; const renderEmoji = ({ - i18n, text, key, sizeClass, @@ -36,7 +35,6 @@ const renderEmoji = ({ renderNonEmoji: RenderTextCallbackType; }) => ( { phoneNumber={contact.phoneNumber} name={contact.name} profileName={contact.profileName} - i18n={i18n} /> {errors.map((error, index) => ( diff --git a/ts/components/conversation/Quote.tsx b/ts/components/conversation/Quote.tsx index 8caebbf4e..9fdd37127 100644 --- a/ts/components/conversation/Quote.tsx +++ b/ts/components/conversation/Quote.tsx @@ -303,7 +303,6 @@ export class Quote extends React.Component { phoneNumber={authorPhoneNumber} name={authorName} profileName={authorProfileName} - i18n={i18n} /> )} diff --git a/ts/components/conversation/SafetyNumberNotification.tsx b/ts/components/conversation/SafetyNumberNotification.tsx index 5e4b4604d..1cd840cfc 100644 --- a/ts/components/conversation/SafetyNumberNotification.tsx +++ b/ts/components/conversation/SafetyNumberNotification.tsx @@ -46,7 +46,6 @@ export class SafetyNumberNotification extends React.Component { className="module-safety-number-notification__contact" > { id={changeKey} components={[ { className="module-unsupported-message__contact" > { id={id} components={[ ( ( - { 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 ( -
- {shortName} -
+ {inline ? ( + // When using this component as a draft.js decorator it is very + // important that these children are the only elements to render + children + ) : ( + {shortName} + )} + ); } ) diff --git a/ts/components/emoji/EmojiButton.tsx b/ts/components/emoji/EmojiButton.tsx index 431c55667..543d4ba0c 100644 --- a/ts/components/emoji/EmojiButton.tsx +++ b/ts/components/emoji/EmojiButton.tsx @@ -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( @@ -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} diff --git a/ts/components/emoji/EmojiPicker.tsx b/ts/components/emoji/EmojiPicker.tsx index c4bfca965..f68583310 100644 --- a/ts/components/emoji/EmojiPicker.tsx +++ b/ts/components/emoji/EmojiPicker.tsx @@ -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; @@ -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 diff --git a/ts/components/emoji/lib.ts b/ts/components/emoji/lib.ts index 007dd6900..072621f78 100644 --- a/ts/components/emoji/lib.ts +++ b/ts/components/emoji/lib.ts @@ -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([ diff --git a/ts/components/stickers/StickerButton.tsx b/ts/components/stickers/StickerButton.tsx index 1f16b0f65..242494b82 100644 --- a/ts/components/stickers/StickerButton.tsx +++ b/ts/components/stickers/StickerButton.tsx @@ -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; } diff --git a/ts/components/stickers/lib.ts b/ts/components/stickers/lib.ts new file mode 100644 index 000000000..f290444ed --- /dev/null +++ b/ts/components/stickers/lib.ts @@ -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; + blessedPacks: ReadonlyArray; + installedPacks: ReadonlyArray; + receivedPacks: ReadonlyArray; +}) { + return ( + o.knownPacks.length + + o.blessedPacks.length + + o.installedPacks.length + + o.receivedPacks.length + ); +} diff --git a/ts/state/ducks/emojis.ts b/ts/state/ducks/emojis.ts index 4af3e6fd3..fcad5ac01 100644 --- a/ts/state/ducks/emojis.ts +++ b/ts/state/ducks/emojis.ts @@ -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), diff --git a/ts/state/roots/createEmojiButton.tsx b/ts/state/roots/createCompositionArea.tsx similarity index 55% rename from ts/state/roots/createEmojiButton.tsx rename to ts/state/roots/createCompositionArea.tsx index c7e29ee7b..7d64f6888 100644 --- a/ts/state/roots/createEmojiButton.tsx +++ b/ts/state/roots/createCompositionArea.tsx @@ -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) => ( - + ); diff --git a/ts/state/roots/createStickerButton.tsx b/ts/state/roots/createStickerButton.tsx deleted file mode 100644 index ade6bb62e..000000000 --- a/ts/state/roots/createStickerButton.tsx +++ /dev/null @@ -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) => ( - - - -); diff --git a/ts/state/smart/StickerButton.tsx b/ts/state/smart/CompositionArea.tsx similarity index 65% rename from ts/state/smart/StickerButton.tsx rename to ts/state/smart/CompositionArea.tsx index ea3914a02..7d64e91f8 100644 --- a/ts/state/smart/StickerButton.tsx +++ b/ts/state/smart/CompositionArea.tsx @@ -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); diff --git a/ts/state/smart/EmojiButton.tsx b/ts/state/smart/EmojiButton.tsx deleted file mode 100644 index a6a118290..000000000 --- a/ts/state/smart/EmojiButton.tsx +++ /dev/null @@ -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, - 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); diff --git a/ts/util/lint/exceptions.json b/ts/util/lint/exceptions.json index d19f8630e..d9da33f5a 100644 --- a/ts/util/lint/exceptions.json +++ b/ts/util/lint/exceptions.json @@ -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