Commit d7ce231c authored by JC Brand's avatar JC Brand

Various emoji improvements:

* Add emoji tooltip
* Make categories configurable and add smileys category
* Rearrange emoji categories and style
& Show all emojis together
parent 4cb9fd88
......@@ -37,6 +37,7 @@
"lodash/prefer-noop": "off",
"lodash/prefer-startswith": "off",
"lodash/preferred-alias": "off",
"lodash/matches-prop-shorthand": "off",
"accessor-pairs": "error",
"array-bracket-spacing": "off",
"array-callback-return": "error",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -112,15 +112,6 @@
text-align: left;
margin: 0 var(--chat-gutter);
img {
&.emoji {
height: 1.2em;
width: 1.2em;
margin: 0 .05em 0 .1em;
vertical-align: -0.1em;
}
}
@media screen and (max-height: $mobile-landscape-height) {
margin: 0;
width: var(--mobile-chat-width);
......@@ -364,64 +355,6 @@
a {
color: var(--link-color);
}
.emoji-picker-container {
background: white;
}
ul {
&.emoji-picker {
overflow-y: scroll;
overflow-x: hidden;
padding: 0.5em;
}
li {
margin-left: 0;
cursor: pointer;
list-style: none;
position: relative;
&.insert-emoji {
padding: 0.2em;
&.picked {
background-color: var(--highlight-color);
}
&:hover {
background-color: var(--highlight-color);
}
a {
font-size: var(--font-size-huge);
&:hover {
color: #8f2831;
}
}
}
}
}
}
&.toggle-smiley {
a.toggle-smiley {
padding: 0;
}
.emoji-toolbar {
box-shadow: 0 -1px 1px 0 rgba(0, 0, 0, 0.4);
.emoji-category-picker {
padding-top: 0.5em;
ul {
display: flex;
flex-direction: row;
justify-content: space-between;
}
}
.emoji-category-picker,
.emoji-skintone-picker {
li {
padding: 0.25em;
font-size: var(--font-size-huge);
&:hover {
background-color: var(--highlight-color);
}
}
}
}
}
&.toggle-otr {
ul {
......@@ -492,11 +425,6 @@
@include make-col(4);
}
}
.emoji-picker {
height: var(--embedded-emoji-picker-height);
}
.chatbox {
min-width: var(--overlayed-chat-width) !important;
width: var(--overlayed-chat-width);
......@@ -526,33 +454,12 @@
.chat-textarea {
max-height: var(--overlayed-max-chat-textarea-height);
}
.emoji-picker {
height: var(--overlayed-emoji-picker-height);
}
.chatbox {
.sendXMPPMessage {
.chat-toolbar {
li {
.toolbar-menu {
min-width: 235px;
ul {
&.emoji-toolbar {
width: 100%;
.emoji-category {
float: left;
}
}
}
}
&.toggle-smiley {
.emoji-toolbar {
.emoji-category-picker {
ul {
flex-wrap: wrap;
justify-content: flex-start;
}
}
}
}
}
}
......@@ -561,15 +468,14 @@
}
@include media-breakpoint-down(sm) {
#conversejs.converse-overlayed {
> .row {
flex-direction: column;
#conversejs.converse-overlayed {
> .row {
flex-direction: column;
&.no-gutters {
margin: -1em;
}
}
}
}
}
}
......@@ -684,9 +590,6 @@
.chat-textarea {
max-height: var(--fullpage-max-chat-textarea-height);
}
.emoji-picker {
height: var(--fullpage-emoji-picker-height);
}
.chatbox {
.box-flyout {
background-color: var(--chat-head-color);
......@@ -714,19 +617,6 @@
ul {
width: 100%;
}
.toggle-smiley {
ul {
&.emoji-toolbar {
.emoji-category-picker {
margin-right: 5em;
}
.emoji-category {
padding-left: 10px;
padding-right: 10px;
}
}
}
}
}
}
}
......
......@@ -324,7 +324,6 @@
}
.sendXMPPMessage {
.suggestion-box__results--above {
bottom: 4.5em;
}
......@@ -338,6 +337,7 @@
color: var(--message-input-color);
}
}
.chat-textarea {
&:active, &:focus{
outline-color: var(--chatroom-head-color);
......
#conversejs {
.chatbox {
img.emoji {
height: 1.2em;
width: 1.2em;
margin: 0 .05em 0 .1em;
vertical-align: -0.1em;
}
.toggle-smiley {
a.toggle-smiley {
padding: 0;
}
.emoji-picker.toolbar-menu {
padding-top: 0;
padding-bottom: 0;
background-color: var(--chat-head-color);
.emoji-picker__container {
overflow-y: hidden;
background: white;
.emoji-picker__lists {
overflow-y: auto;
.emoji-category__heading {
color: var(--subdued-color);
font-size: var(--font-size);
padding: 0.5em 0 0 0.5em;
}
display: flex;
flex-direction: column;
}
.emoji-skintone-picker {
padding: 0.25em 0;
background-color: var(--chat-head-color);
width: auto;
font-size: var(--font-size-huge);
&:hover {
background-color: var(--highlight-color);
}
}
}
.emoji-picker {
background-color: white;
padding: 0.5em;
li {
margin-left: 0;
cursor: pointer;
list-style: none;
position: relative;
&.insert-emoji {
padding: 0.2em;
&.picked {
background-color: var(--highlight-color);
}
&:hover {
background-color: var(--highlight-color);
}
a {
font-size: var(--font-size-huge);
}
}
}
}
.emoji-category-picker {
padding-top: 0.5em;
background-color: var(--chat-head-color);
ul {
display: flex;
flex-direction: row;
justify-content: space-between;
.emoji-category {
&.picked {
background-color: white;
border: 1px var(--chat-head-color) solid;
border-bottom: none;
}
padding: 0.25em;
font-size: var(--font-size-huge);
&:hover {
background-color: var(--highlight-color);
}
}
}
}
}
}
}
.chatroom {
.toggle-smiley {
.emoji-picker.toolbar-menu {
background-color: var(--chatroom-head-color);
.emoji-picker__container {
background: white;
.emoji-skintone-picker {
background-color: var(--chatroom-head-color);
}
.emoji-category-picker {
background-color: var(--chatroom-head-color);
.emoji-category {
&.picked {
border: 1px var(--chatroom-head-color) solid;
border-bottom: none;
}
}
}
}
}
}
}
}
#conversejs.converse-embedded,
#conversejs.converse-overlayed {
.emoji-picker__container {
height: var(--embedded-emoji-picker-height);
.emoji-picker__lists {
height: calc(var(--embedded-emoji-picker-height) - 4em;
}
}
}
#conversejs.converse-overlayed {
.emoji-picker__container {
height: var(--overlayed-emoji-picker-height);
}
.chatbox {
.toggle-smiley {
.emoji-picker.toolbar-menu {
.emoji-picker__container {
.emoji-picker {
.insert-emoji {
a {
font-size: var(--font-size);
}
}
}
.emoji-skintone-picker {
font-size: var(--font-size-small);
}
.emoji-category-picker {
.emoji-category {
font-size: var(--font-size);
}
}
}
}
}
}
}
#conversejs.converse-fullscreen {
.emoji-picker__container {
height: var(--fullpage-emoji-picker-height);
.emoji-picker__lists {
height: calc(var(--fullpage-emoji-picker-height) - 4em;
}
}
.chatbox {
.sendXMPPMessage {
.toggle-smiley {
.emoji-category {
padding-left: 0.2em;
padding-right: 0.2em;
}
}
}
}
}
......@@ -43,8 +43,8 @@ $mobile_portrait_length: 480px !default;
--send-button-height: 27px;
--send-button-margin: 3px;
--controlbox-heading-top-margin: 0.75em;
--inline-action-margin: 0.75em;
--controlbox-heading-top-margin: 0.75em;
--inline-action-margin: 0.75em;
--roster-height: 194px;
......
......@@ -63,3 +63,4 @@
@import "minimized_chats";
@import "bookmarks";
@import "autocomplete";
@import "emoji";
......@@ -927,7 +927,7 @@
it("will display larger if it's a single emoji",
mock.initConverse(
null, ['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'], {},
null, ['rosterGroupsFetched', 'chatBoxesFetched', 'emojisInitialized'], {'use_system_emojis': false},
async function (done, _converse) {
await test_utils.waitForRoster(_converse, 'current');
......
......@@ -119,8 +119,9 @@ converse.plugins.add('converse-chatview', {
Object.assign(
this.model.toJSON(), {
'_': _,
'emoji_categories': _converse.emojis.categories,
'emojis_by_category': _converse.emojis.by_category,
'_converse': _converse,
'emoji_categories': _converse.emoji_categories,
'emojis_by_category': u.getEmojisByCategory(),
'shouldBeHidden': this.shouldBeHidden,
'skintones': ['tone1', 'tone2', 'tone3', 'tone4', 'tone5'],
'toned_emojis': _converse.emojis.toned,
......@@ -162,10 +163,7 @@ converse.plugins.add('converse-chatview', {
},
chooseCategory (ev) {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target.nodeName === 'IMG' ?
ev.target.parentElement : ev.target;
const target = ev.target.nodeName === 'IMG' ? ev.target.parentElement : ev.target;
const category = target.getAttribute("data-category").trim();
this.model.save({
'current_category': category,
......
......@@ -9,9 +9,9 @@
import * as twemoji from "twemoji";
import _ from "./lodash.noconflict";
import converse from "./converse-core";
import u from "./utils/core";
const { Strophe } = converse.env;
const u = converse.env.utils;
const ASCII_LIST = {
'*\\0/*':'1f646',
......@@ -137,9 +137,8 @@ const ASCII_REPLACE_REGEX = new RegExp("<object[^>]*>.*?<\/object>|<span[^>]*>.*
function convert (unicode) {
/* For converting unicode code points and code pairs
* to their respective characters
*/
// Converts unicode code points and code pairs
// to their respective characters
if (unicode.indexOf("-") > -1) {
const parts = [],
s = unicode.split('-');
......@@ -171,15 +170,45 @@ converse.plugins.add('converse-emoji', {
_converse.api.settings.update({
'emoji_image_path': twemoji.default.base,
'emoji_json_path': '/dist/emojis.json'
'emoji_json_path': '/dist/emojis.json',
'emoji_categories': {
"smileys": ":grinning:",
"people": ":thumbsup:",
"activity": ":soccer:",
"travel": ":motorcycle:",
"objects": ":bomb:",
"nature": ":rainbow:",
"food": ":hotdog:",
"symbols": ":musical_note:",
"flags": ":flag_ac:"
}
});
_converse.api.promises.add(['emojisInitialized']);
twemoji.default.base = _converse.emoji_image_path;
_converse.emoji_category_labels = {
"smileys": __("Smileys and emotions"),
"people": __("People"),
"activity": __("Activities"),
"travel": __("Travel"),
"objects": __("Objects"),
"nature": __("Animals and nature"),
"food": __("Food and drink"),
"symbols": __("Symbols"),
"flags": __("Flags")
}
_converse.emojis = {};
u.getEmojiRenderer = function () {
return _converse.use_system_emojis ? u.shortnameToUnicode : _.flow(u.shortnameToUnicode, twemoji.default.parse);
const how = {
'attributes': (icon, variant) => {
const codepoint = twemoji.default.convert.toCodePoint(icon);
return {'title': `${u.getEmojisByAtrribute('cp')[codepoint]['sn']} ${icon}`}
}
};
const toUnicode = u.shortnameToUnicode;
return _converse.use_system_emojis ? toUnicode: text => twemoji.default.parse(toUnicode(text), how);
};
u.addEmoji = function (text) {
......@@ -189,105 +218,21 @@ converse.plugins.add('converse-emoji', {
function getTonedEmojis () {
if (!_converse.toned_emojis) {
_converse.toned_emojis = _.uniq(
_.map(
_.filter(
_converse.emojis.by_category.people,
person => _.includes(person._shortname, '_tone')
),
person => person._shortname.replace(/_tone[1-5]/, '')
)
u.getEmojisByCategory().people
.filter(person => person.sn.includes('_tone'))
.map(person => person.sn.replace(/_tone[1-5]/, ''))
);
}
return _converse.toned_emojis;
}
function getEmojisByCategory () {
/* Return a dict of emojis with the categories as keys and
* lists of emojis in that category as values.
*/
const emojis = Object.values(_.mapValues(_converse.emojis.json, function (value, key, o) {
value._shortname = key;
return value
}));
const tones = [':tone1:', ':tone2:', ':tone3:', ':tone4:', ':tone5:'];
const excluded = [':kiss_ww:', ':kiss_mm:', ':kiss_woman_man:'];
const excluded_substrings = [
':woman', ':man', ':women_', ':men_', '_man_', '_woman_', '_woman:', '_man:'
];
const excluded_categories = ['modifier', 'regional'];
const categories = _.difference(
_.uniq(_.map(emojis, _.partial(_.get, _, 'category'))),
excluded_categories
);
const emojis_by_category = {};
_.forEach(categories, (cat) => {
let list = _.sortBy(_.filter(emojis, ['category', cat]), ['uc_base']);
list = _.filter(
list,
(item) => !_.includes(_.concat(tones, excluded), item._shortname) &&
!_.some(excluded_substrings, _.partial(_.includes, item._shortname))
);
if (cat === 'people') {
const idx = _.findIndex(list, ['uc_base', '1f600']);
list = _.union(_.slice(list, idx), _.slice(list, 0, idx+1));
} else if (cat === 'activity') {
list = _.union(_.slice(list, 27-1), _.slice(list, 0, 27));
} else if (cat === 'objects') {
list = _.union(_.slice(list, 24-1), _.slice(list, 0, 24));
} else if (cat === 'travel') {
list = _.union(_.slice(list, 17-1), _.slice(list, 0, 17));
} else if (cat === 'symbols') {
list = _.union(_.slice(list, 60-1), _.slice(list, 0, 60));
}
emojis_by_category[cat] = list;
});
return emojis_by_category;
}
u.isSingleEmoji = function (str) {
str = str.trim();
if (!str || (str.length > 2 && !str.startsWith(':'))) {
return;
}
const result = _.flow(u.shortnameToUnicode, twemoji.default.parse)(str)
const match = result.match(/<img class="emoji" draggable="false" alt=".*?" src=".*?\.png"\/>/);
return match && match.length === 1;
}
/**
* Returns unicode represented by the psased in shortname.
* @private
* @param {string} str - String containg the shortname(s)
*/
u.shortnameToUnicode = function (str) {
str = str.replace(_converse.emojis.shortnames_regex, shortname => {
if( (typeof shortname === 'undefined') || (shortname === '') || (!(shortname in _converse.emojis.json)) ) {
// if the shortname doesnt exist just return the entire match
return shortname;
}
const unicode = _converse.emojis.json[shortname].uc_output.toUpperCase();
return convert(unicode);
});
// Also replace ASCII smileys
str = str.replace(ASCII_REPLACE_REGEX, (entire, m1, m2, m3) => {
if( (typeof m3 === 'undefined') || (m3 === '') || (!(u.unescapeHTML(m3) in ASCII_LIST)) ) {
// if the ascii doesnt exist just return the entire match
return entire;
}
m3 = u.unescapeHTML(m3);
const unicode = ASCII_LIST[m3].toUpperCase();
return m2+convert(unicode);
});
return str;
}
function getShortNames () {
const shortnames = [];
for (const emoji in _converse.emojis.json) {
if (!Object.prototype.hasOwnProperty.call(_converse.emojis.json, emoji) || (emoji === '')) continue;
shortnames.push(emoji.replace(/[+]/g, "\\$&"));
for (let i = 0; i < _converse.emojis.json[emoji].shortnames.length; i++) {
shortnames.push(_converse.emojis.json[emoji].shortnames[i].replace(/[+]/g, "\\$&"));
for (let i = 0; i < _converse.emojis.json[emoji].sns.length; i++) {
shortnames.push(_converse.emojis.json[emoji].sns[i].replace(/[+]/g, "\\$&"));
}
}
return shortnames.join('|');
......@@ -323,11 +268,121 @@ converse.plugins.add('converse-emoji', {
return promise;
}
/************************ BEGIN Utils ************************/
// Closured cache
const emojis_by_attribute = {};
Object.assign(u, {
/**
* @method u.shortnameToUnicode
* Returns unicode represented by the passed in shortname.
* @param {string} str - String containg the shortname(s)
*/
shortnameToUnicode (str) {
str = str.replace(_converse.emojis.shortnames_regex, shortname => {
if( (typeof shortname === 'undefined') || (shortname === '') || (!(shortname in _converse.emojis.json)) ) {
// if the shortname doesnt exist just return the entire match
return shortname;
}
const unicode = _converse.emojis.json[shortname].cp.toUpperCase();
return convert(unicode);
});
// Also replace ASCII smileys
str = str.replace(ASCII_REPLACE_REGEX, (entire, m1, m2, m3) => {
if( (typeof m3 === 'undefined') || (m3 === '') || (!(u.unescapeHTML(m3) in ASCII_LIST)) ) {
// if the ascii doesnt exist just return the entire match
return entire;
}
m3 = u.unescapeHTML(m3);
const unicode = ASCII_LIST[m3].toUpperCase();
return m2+convert(unicode);
});
return str;
},
/**
* Determines whether the passed in string is just a single emoji shortname;
* @method u.isSingleEmoji
* @param {string} shortname - A string which migh be just an emoji shortname
* @returns {boolean}
*/
isSingleEmoji (shortname) {
shortname = shortname.trim();
if (!shortname || (shortname.length > 2 && !shortname.startsWith(':'))) {
return;
}
const result = twemoji.default.parse(u.shortnameToUnicode(shortname));
const match = result.match(/<img class="emoji" draggable="false" alt=".*?" src=".*?\.png"\/>/);
return match && match.length === 1;
},
/**
* @method u.getEmojisByAtrribute
* @param {string} attr - The attribute according to which the
* returned map should be keyed.
* @returns {object} - Map of emojis with the passed in attribute values
* as keys and a list of emojis for a particular category as values.
*/
getEmojisByAtrribute (attr) {
if (emojis_by_attribute[attr]) {
return emojis_by_attribute[attr];
}
if (attr === 'category') {
return u.getEmojisByCategory();
}
emojis_by_attribute[attr] = {};
const all_variants = _converse.emojis_list
.map(e => e[attr])
.filter((c, i, arr) => arr.indexOf(c) == i);
all_variants.forEach(v => {
emojis_by_attribute[attr][v] = _.find(_converse.emojis_list, i => (i[attr] === v));
});
return emojis_by_attribute[attr];
},
/**
* @method u.getEmojisByCategory
* @returns {object} - Map of emojis with categories as keys
* and a list of emojis for a particular category as values.
*/
getEmojisByCategory () {
if (emojis_by_attribute['category']) {
return emojis_by_attribute['category'];
}
const tones = [':tone1:', ':tone2:', ':tone3:', ':tone4:', ':tone5:'];
const excluded = [':kiss_ww:', ':kiss_mm:', ':kiss_woman_man:'];
const excluded_substrings = [':woman', ':man', ':women_', ':men_', '_man_', '_woman_', '_woman:', '_man:'];
const is_excluded = sn => [...tones, ...excluded].includes(sn);
const has_excluded_substring = sn => excluded_substrings.reduce((out, str) => (out || sn.includes(str)), false);
emojis_by_attribute['category'] = {};
_converse.emojis.all_categories.forEach(cat => {
let list = _.sortBy(_converse.emojis_list.filter(e => e.c === cat), ['cp']);
list = list.filter(item => (!is_excluded(item.sn) && !has_excluded_substring(item.sn)));
if (cat === 'smileys') {
const idx = _.findIndex(list, ['cp', '1f600']);
list = _.union(_.slice(list, idx), _.slice(list, 0, idx+1));
}
emojis_by_attribute['category'][cat] = list;
});
return emojis_by_attribute['category'];
}
});
/************************ END Utils ************************/
await fetchEmojiJSON();
_converse.emojis.shortnames_regex = new RegExp("<object[^>]*>.*?<\/object>|<span[^>]*>.*?<\/span>|<(?:object|embed|svg|img|div|span|p|a)[^>]*>|("+getShortNames()+")", "gi");
_converse.emojis.by_category = getEmojisByCategory();
_converse.emojis.categories = ["people", "activity", "travel", "objects", "nature", "food", "symbols", "flags"];
_converse.emojis_list = Object.values(_converse.emojis.json);
const excluded_categories = ['modifier', 'regional'];
_converse.emojis.all_categories = _converse.emojis_list
.map(e => e.c)
.filter((c, i, arr) => arr.indexOf(c) == i)
.filter(c => !excluded_categories.includes(c));
_converse.emojis.toned = getTonedEmojis();
/**
* Triggered once the JSON file representing emoji data has been
* fetched and its save to start calling emoji utility methods.
......
<div class="emoji-picker-container">
{[ o.emoji_categories.forEach(function (category) { ]}
<ul class="emoji-picker emoji-picker-{{{category}}} {[ if (o.current_category !== category) { ]} hidden {[ } ]}">
{[ o._.forEach(o.emojis_by_category[category], function (emoji) { ]}
<li class="emoji insert-emoji {[ if (o.shouldBeHidden(emoji._shortname, o.current_skintone, o.toned_emojis)) { ]} hidden {[ }; ]}"
data-emoji="{{{emoji._shortname}}}" title="{{{emoji._shortname}}}">
<a href="#" data-emoji="{{{emoji._shortname}}}"> {{ o.transform(emoji._shortname) }} </a>
</li>
{[ }); ]}
</ul>
{[ }); ]}
<ul class="emoji-toolbar">
<li class="emoji-category-picker">
<div class="emoji-picker__container">
<div class="emoji-category-picker">
<ul>
{[ o.emoji_categories.forEach(function (category) { ]}
{[ Object.keys(o.emoji_categories).forEach(function (category) { ]}
<li data-category="{{{category}}}" class="emoji-category {[ if (o.current_category === category) { ]} picked {[ } ]}">
<a class="pick-category" href="#" data-category="{{{category}}}"> {{ o.transform(o.emojis_by_category[category][0]._shortname) }} </a>
<a class="pick-category" href="#emoji-picker-{{{category}}}" data-category="{{{category}}}"> {{ o.transform(o.emoji_categories[category]) }} </a>
</li>
{[ }); ]}
</ul>
</li>
<li class="emoji-skintone-picker">
<ul>
{[ o._.forEach(o.skintones, function (skintone) { ]}
<li data-skintone="{{{skintone}}}" class="emoji-skintone {[ if (o.current_skintone === skintone) { ]} picked {[ } ]}">
<a class="pick-skintone" href="#" data-skintone="{{{skintone}}}"> {{ o.transform(':'+skintone+':') }} </a>
</div>
<div class="emoji-picker__lists">
{[ Object.keys(o.emoji_categories).forEach(function (category) { ]}
<a id="emoji-picker-{{{category}}}" class="emoji-category__heading">{{{o._converse.emoji_category_labels[category]}}}</a>
<ul class="emoji-picker emoji-picker-{{{category}}}">
{[ o._.forEach(o.emojis_by_category[category], function (emoji) { ]}
<li class="emoji insert-emoji {[ if (o.shouldBeHidden(emoji.sn, o.current_skintone, o.toned_emojis)) { ]} hidden {[ }; ]}"
data-emoji="{{{emoji.sn}}}" title="{{{emoji.sn}}}">
<a href="#" data-emoji="{{{emoji.sn}}}"> {{ o.transform(emoji.sn) }} </a>
</li>
{[ }); ]}
</ul>
</li>
</ul>
{[ }); ]}
</ul>
{[ }); ]}
</div>
<ul class="emoji-skintone-picker">
{[ o._.forEach(o.skintones, function (skintone) { ]}
<li data-skintone="{{{skintone}}}" class="emoji-skintone {[ if (o.current_skintone === skintone) { ]} picked {[ } ]}">
<a class="pick-skintone" href="#" data-skintone="{{{skintone}}}"> {{ o.transform(':'+skintone+':') }} </a>
</li>
{[ }); ]}
</ul>
</div>
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment