company_banner

Эмодзи?! Нет, не слышал

    image В нашу жизнь уже давно вошли эмодзи. И в социальных сетях, и во всевозможных мессенджерах мы используем их не задумываясь, выражая свои эмоции всего одним символом. Но для кроссплатформенного приложения отправка и отображение эмодзи — непростая задача. Проблема заключается в том, что отправленные эмодзи с мобильных приложений не всегда отображаются корректно на веб-сайтах.

    Последние версии iOS и Android имеют поддержку более 1200 символов эмодзи, но «десктопный» рынок не может похвастаться такими успехами. Мы же в Badoo хотим и делаем все, чтобы пользователям было комфортно общаться на всех платформах, не имея никаких ограничений в переписке.
    Далее я расскажу, каким способом мы добились 100% поддержки эмодзи для веба.


    Вот так бы пользователь Windows увидел сообщение в браузере без эмодзи:
    image


    Основная идея состоит в том, что мы берем любой символ эмодзи, определяем его Юникод-код и заменяем на html-элемент, который будет корректно отображаться в браузере.

    Теория


    Рассмотрим image(улыбающееся лицо). Он имеет код U+1F600. Как получить его код с помощью JavaScript:

    'image'.length //2
    'image'.charCodeAt(0).toString(16) // D83D
    'image'.charCodeAt(1).toString(16) // DE00

    В итоге мы получили суррогатную пару: U+D83D U+DE00.

    UTF-16 кодирует символы в виде последовательности 16-битных слов, это позволяет записывать символы Юникода в диапазонах от U+0000 до U+D7FF и от U+E000 до U+10FFFF (общим количеством 1 112 064). Если требуется представить в UTF-16 символ с кодом больше U+FFFF, то используются два слова: первая часть суррогатной пары (в диапазоне от 0xD800 до 0xDBFF) и вторая (от 0xDC00 до 0xDFFF).

    Чтобы получить код эмодзи, который находится в диапазоне больше U+FFFF, воспользуемся формулой:

    (0xD83D - 0xD800) * 0x400 + 0xDE00 - 0xDC00 + 0x10000 = 1f600
    

    А теперь переведем обратно:

    D83D = ((0x1f600 - 0x10000) >> 10) + 0xD800;
    DE00 = ((0x1f600 - 0x10000) % 0x400) + 0xDC00;
    


    Это довольно сложно и неудобно, рассмотрим, что нам может предложить ES 2015.

    С новым стандартом JavaScript можно забыть про суррогатные пары и облегчить себе жизнь:

    String.prototype.codePointAt // возвращает код из символа,
    String.fromCodePoint // возвращает символ из кода.
    

    Оба метода корректно работают с суррогатными парами.

    Возможность вставки восьмизначных кодов в строку:
    \u{1F466} вместо \uD83D\uDC66

    RegExp.prototype.unicode: флаг u в регулярных выражениях дает лучшую поддержку при работе Юникодом:

    /\u{1F466}/u
    


    На данный момент стандарт Юникод 8.0 содержит 1281 символов эмодзи, и это не считая модификаторов цвета кожи и групп (эмодзи семьи). Существуют различные реализации от известных компаний:
    image


    Эмодзи можно разделить на несколько групп:

    • простые: в диапозоне до 0xD7FF — image;
    • суррогатные пары: от 0xD800 до 0xDFFF — image;
    • числа: от 0x0023 до 0x0039 + 0x20E3 — image;
    • государственные флаги: 2 символа от 0xDDE6 до 0xDDFF, в результате — image;
    • модификаторы цвета кожи: image + от 0xDFFB до 0xDFFF — image;
    • семья: последовательность из image image image соединенных 0x200D или 0x200C — image


    Решение:


    1. получаем исходный текст с символом, ищем в нем с помощью регулярного выражения все наборы эмодзи;
    2. определяем код символа с помощью функции codePointAt;
    3. создаем элемент img (важно, чтобы это был именно тег img) с url, который состоит из кода этого символа;
    4. заменяем символ на img в исходном тексте.

    function emojiToHtml(str) {
    	str = str.replace(/\uFE0F/g, '');
    	return str.replace(emojiRegex, buildImgFromEmoji);
    }
    
    var tpl = '<img class="emoji emoji--{code} js-smile-insert" src="{src}" srcset="{src} 1x, {src_x2} 2x" unselectable="on">';
    var url = 'https://badoocdn.com/big/chat/emoji/{code}.png';
    var url2 = 'https://badoocdn.com/big/chat/emoji@x2/{code}.png';
    function buildImgFromEmoji(emoji) {
    	var codePoint = extractEmojiToCodePoint(emoji);
    	return $tpl(tpl, {
    		code: codePoint,
    		src: $tpl(url, {
    			code: codePoint
    		}),
    		src_x2: $tpl(url2, {
    			code: codePoint
    		})
    	});
    }
    
    function extractEmojiToCodePoint(emoji) {
    	return emoji
    		.split('')
    		.map(function (symbol, index) {
    			return emoji.codePointAt(index).toString(16);
    		})
    		.filter(function (codePoint) {
    			return !isSurrogatePair(codePoint);
    		}, this)
    		.join('-');
    }
    
    function isSurrogatePair(codePoint) {
    	codePoint = parseInt(codePoint, 16);
    	return codePoint >= 0xD800 && codePoint <= 0xDFFF;
    }
    


    Основная идея в регулярном выражении, которое находит символы эмодзи:

    var emojiRanges = [
    	'(?:\uD83C[\uDDE6-\uDDFF]){2}', // флаги
    	'[\u0023-\u0039]\u20E3', // числа
    	'(?:[\uD83D\uD83C\uD83E][\uDC00-\uDFFF]|[\u270A-\u270D\u261D\u26F9])\uD83C[\uDFFB-\uDFFF]', // цвет кожи
    	'\uD83D[\uDC68\uDC69][\u200D\u200C].+?\uD83D[\uDC66-\uDC69](?![\u200D\u200C])', // семья
    	'[\uD83D\uD83C\uD83E][\uDC00-\uDFFF]', // суррогатная пара
    	'[\u3297\u3299\u303D\u2B50\u2B55\u2B1B\u27BF\u27A1\u24C2\u25B6\u25C0\u2600\u2705\u21AA\u21A9]', // обычные
    	'[\u203C\u2049\u2122\u2328\u2601\u260E\u261d\u2620\u2626\u262A\u2638\u2639\u263a\u267B\u267F\u2702\u2708]',
    	'[\u2194-\u2199]',
    	'[\u2B05-\u2B07]',
    	'[\u2934-\u2935]',
    	'[\u2795-\u2797]',
    	'[\u2709-\u2764]',
    	'[\u2622-\u2623]',
    	'[\u262E-\u262F]',
    	'[\u231A-\u231B]',
    	'[\u23E9-\u23EF]',
    	'[\u23F0-\u23F4]',
    	'[\u23F8-\u23FA]',
    	'[\u25AA-\u25AB]',
    	'[\u25FB-\u25FE]',
    	'[\u2602-\u2618]',
    	'[\u2648-\u2653]',
    	'[\u2660-\u2668]',
    	'[\u26A0-\u26FA]',
    	'[\u2692-\u269C]'
    ];
    var emojiRegex = new RegExp(emojiRanges.join('|'), 'g');
    


    Чат


    Далее рассмотрим, каким образом можно построить прототип чата с поддержкой эмодзи.

    В качестве поля для ввода сообщения используется div:

    <div id="t" contenteditable="true" data-placeholder="Введите сообщение"></div>
    


    При вводе сообщения или вставки из буфера обмена мы будем чистить его содержимое от возможных html-тегов:

    var tagRegex = /<[^>]+>/gim;
    var styleTagRegex = /<style\b[^>]*>([\s\S]*?)<\/style>/gim;
    var validTagsRegex = /<br[\s/]*>|<img\s+class="emoji\semoji[-\w\s]+"\s+((src|srcset|unselectable)="[^"]*"\s*)+>/i;
    
    function cleanUp(text) {
    	return text
    		.replace(styleTagRegex, '')
    		.replace(tagRegex, function (tag) {
    			return tag.match(validTagsRegex) ? tag : '';
    		})
    		.replace(/\n/g, '');
    }
    


    Для обработки строки, вставленной из буфера обмена, используем событие paste:

    function onPaste(e) {
    	e.preventDefault();
    	var clp = e.clipboardData;
    
    	if (clp !== undefined || window.clipboardData !== undefined) {
    		var text;
    
    		if (clp !== undefined) {
    			text = clp.getData('text/html') || clp.getData('text/plain') || '';
    		} else {
    			text = window.clipboardData.getData('text') || '';
    		}
    
    		if (text) {
    			text = cleanUp(text);
    			text = emojiToHtml(text);
    			var el = document.createElement('span');
    			el.innerHTML = text;
    			el.innerHTML = el.innerHTML.replace(/\n/g, '');
    			t.appendChild(el);
    			restore();
    		}
    	}
    }
    


    Затем заменяем все найденные эмодзи на html-тег img, как было показано выше. Именно на img, так как contenteditable лучше всего работает с ним. С другими элементами могут возникать баги при редактировании.

    После вставки img в поле для ввода требуется восстановить позицию каретки, чтобы пользователь мог продолжить набор сообщения. Для этого используем JavaScript объекты Selection и Range:

    function restore() {
    	var range = document.createRange();
    	range.selectNodeContents(t);
    	range.collapse(false);
    	var sel = window.getSelection();
    	sel.removeAllRanges();
    	sel.addRange(range);
    }
    


    После того как набор сообщения завершен, требуется проделать обратную процедуру. А именно превратить img в символ для отправки на сервер с помощью функции fromCodePoint:

    var htmlToEmojiRegex = /<img.*?class="emoji\semoji--(.+?)\sjs-smile-insert".*?>/gi;
    function htmlToEmoji(html) {
    	return html.replace(htmlToEmojiRegex, function (imgTag, codesStr) {
    		var codesInt = codesStr.split('-').map(function (codePoint) {
    			return parseInt(codePoint, 16);
    		});
    
    		var emoji = String.fromCodePoint.apply(null, codesInt);
    
    		return emoji.match(emojiRegex) ? emoji : '';
    	});
    }
    


    Посмотреть пример чата можно тут: https://jsfiddle.net/q9484hcc/

    Так мы разработали поддержку эмодзи, чтобы наши пользователи могли выражать эмоции в полной мере и общаться друг с другом без ограничений. Если у вас есть идеи по улучшению наших методов или их изменению ― пишите в комментариях, с удовольствием их обсудим!

    Полезные ссылки:
    http://emojipedia.org/
    http://getemoji.com/
    Полифил String.fromCodePoint
    Полифил String.prototype.codePointAt

    Артем Кунец
    Frontend-разработчик Badoo
    Badoo
    457.01
    Big Dating
    Share post

    Comments 32

      +8
      Давайте обсудим! У меня они отключены везде где только можно и на показ и на отправку по двум причинам.
      Первая — лично меня они визуально раздражают. Вторая — считаю, что выражать свои эмоции гораздо лучше текстом, нежели шариками. (Исключая "!", "?" и простой смайлик типа ":)", но текстом.

        +8
        Только вот для сервисов знакомств с соответствующей аудиторией они прям очень нужны.
          +3
          https://www.artlebedev.ru/kovodstvo/sections/135/

          Просто поделюсь ссылкой.
            +4
            Да, но этому руководству уже 10 лет. Мир не стоит на месте. И пусть многим это не нравится, но смайлики плотно пришли в нашу жизнь.
            +17
            «Инструкция: как приготовить стейк».
            Первый комментатор: а я вот вегетарианец, и вообще мясо вредно!

            No offense ;)
              +6
              Вам показали решение технической проблемы. А вы пишете о ваших личных предпочтениях в типографии.
              +1
              Почему обязательно нужно было «мучиться» с UTF-16? UTF-8 не способен кодировать те же юникод-символы?
                0
                Потому что когда делали Web всё были в эйфории и верили, что «65536 символов хватит всем». Теперь расплачиваемся.

                Та же самая проблема, что и в Java, собственно…
                  +1

                  Способен, конечно, но в Javascript (как и в Java, и во многих других языках, особенно на винде) используются UCS-2-строки. Из-за этого возникает много проблем, см. http://utf8everywhere.org/.

                    +1
                    И правильно что используются. Современный Юникод пошел по неправильному пути развития, UTF-16 вообще не нужен (есть универсальные UTF-8 и UTF-32), а вот UCS-2 нужен (как и ASCII) — как ограниченные подмножества реально используемых символов с фиксированной битовой длиной символа. Но у UCS-2 и ASCII должен быть другой статус — не «универсальные» кодировки, а «технические» — для применения внутри программ и операционных систем там, где может потребоваться индексация массива символов за константное время.
                      0
                      для применения внутри программ и операционных систем там, где может потребоваться индексация массива символов за константное время
                      Я часто слышу этот рефрен но очень редко вижу примеры. Когда это конкретно нужно? Зачем? Что мы пытаемся делать?

                      Я знаю алгоритмы, которые имеет смысл делать с однобайтовыми кодировками (словари к примеру): сильное ограничение, да — но с этим можно жить, если чётко понимать, что происходит. Но что и когда можно делать с UCS-2? Какую практическую задачу с его помощью можно решить? И зачем для этой задачи UCS-2?

                      P.S. Я знаю много неправильных ответов на этот вопрос (в смысле: задач, где USC-2 кажется уместным, но где он не работает), а вот реальных задач где его бы можно было применить… нет, не видел.
                        0

                        Почитайте, пожалуйста, сайт, на который я скидывал ссылку. Все эти аргументы там разжёваны по много раз. Например, в 99% случаев для работы с текстом фиксированная длина code unit'а абсолютно не нужна.

                    0
                    В Windows 10 вполне нормально отображаются Emoji. И вводить их можно, с помощью экранной клавиатуры.
                      +1
                      У меня на Windows 10 выглядит так:
                      image

                      Хочется все таки, полной поддержки эмодзи, которые могут прислать с мобильных устройств.
                        0
                        У вас какая-то неправильная Windows 10. (А на самом деле скорее всего дело в шрифтах) http://i.imgur.com/niA2GKS.png
                          +1
                          Windows 10 поддерживает некоторый набор эмодзи, но не все.
                          Вы можете посмотреть, что поддерживается у вас на http://getemoji.com/
                            0
                            На последней сборке в Insider Preview набор поддерживаемых эмодзи куда больше i.imgur.com/KLGNV5T.png
                      0
                      А подключить веб-шрифт с необходимыми символами разве не проще? Или хотелось обязательно цветных эмодзи?
                        +3
                        Пробывали сначала шрифтом, но продакт-менеджерам хотелось именно цветных и красивых.
                          +7
                          «Пробовали».
                            0
                            «Ретина» поддерживается, надеюсь?
                              +1
                              srcset="{src} 1x, {src_x2} 2x"
                              

                              да
                          +9
                          У вас не хватает вписанного в img тег alt текста с оригинальным emoji, чтобы можно было копировать
                            +2
                            Спасибо за идею, можно реализовать в будущем.
                            0
                            Вопрос: зачем нужно «превратить img в символ для отправки на сервер с помощью функции fromCodePoint», если символ можно просто сохранить as is? Причём сохранить где угодно, хоть в самом созданном элементе img. И как угодно: хоть как data-атрибут, хоть как поле объекта HTMLIMGElement, хоть как элемент стороннего объекта/массива, хоть в независимую переменную (тьфу-тьфу-тьфу).
                            Если сообщение будет состоять из 100 смайликов, будет для каждого вызываться функция htmlToEmoji? Как-то затратно получается.
                              0
                              У нас существует несколько возможностей для вставки эмодзи: подготовленный набор эмодзи, которые пользователь может использовать в интерфейсе чата; последовательность символов, таких как :-) :D и т.д.
                              Решили, что будем использовать единый формат в виде Юникод-кода для всех этих случаев.
                                0
                                Я не спорю, что единый формат лучше. Я спрашиваю, зачем код этого формата надо извлекать функцией и регекспом из html-кода элемента <img>, если можно легко и непринуждённо извлечь из места, куда он заранее положен в чистом виде, хоть из атрибута alt, как выше предложили.
                              +2
                              Следует упомянуть, что по адресу https://github.com/twitter/twemoji/ на Гитхабе расположено твиттеровское решение этой проблемы (с открытым исходным кодом, как это чаще всего и случается на Гитхабе), и упоминаю.
                                +3
                                И всё же самые лучшие смайлики — это колобки.
                                  0
                                  qip смайлики мне до сих пор вспоминаются как озорные и прикольные :)
                                    +2
                                    да, они действительно крутые! Особенно image
                                  0

                                  Only users with full accounts can post comments. Log in, please.