소스 검색

Add initial support for custom emojis

JC Brand 5 년 전
부모
커밋
e6e23a1a82

+ 3 - 0
CHANGES.md

@@ -6,6 +6,9 @@
 - #1691 Fix `collection.chatbox is undefined` errors
 - #1733 New message notifications for a minimized chat stack on top of each other
 - Prevent editing of sent file uploads.
+- Initial support for sending custom emojis. Currently only between Converse
+  instances. Still working out a wire protocol for compatibility with other clients.
+  To add custom emojis, edit the `emojis.json` file.
 
 ### Breaking changes
 

+ 1 - 1
Makefile

@@ -68,7 +68,7 @@ serve_bg: stamp-npm
 dist/converse-no-dependencies.js: src webpack.common.js webpack.nodeps.js stamp-npm @converse/headless
 	npm run nodeps
 
-GETTEXT = $(XGETTEXT) --from-code=UTF-8 --language=JavaScript --keyword=__ -keyword=___ --force-po --output=locale/converse.pot --package-name=Converse.js --copyright-holder="Jan-Carel Brand" --package-version=5.0.4 dist/converse-no-dependencies.js -c
+GETTEXT = $(XGETTEXT) --from-code=UTF-8 --language=JavaScript --keyword=__ --keyword=___ --force-po --output=locale/converse.pot --package-name=Converse.js --copyright-holder="Jan-Carel Brand" --package-version=5.0.4 dist/converse-no-dependencies.js -c
 
 .PHONY: pot
 pot: dist/converse-no-dependencies.js

BIN
dist/custom_emojis/converse.png


BIN
dist/custom_emojis/xmpp.png


+ 57 - 0
docs/source/configuration.rst

@@ -664,6 +664,63 @@ domain_placeholder
 The placeholder text shown in the domain input on the registration form.
 
 
+emoji_categories
+----------------
+
+* Default:::
+  {
+    "smileys": ":grinning:",
+    "people": ":thumbsup:",
+    "activity": ":soccer:",
+    "travel": ":motorcycle:",
+    "objects": ":bomb:",
+    "nature": ":rainbow:",
+    "food": ":hotdog:",
+    "symbols": ":musical_note:",
+    "flags": ":flag_ac:",
+    "custom": ":converse:"
+  }
+
+This setting lets you define the categories that are available in the emoji
+picker, as well as the default image that's shown for each category.
+
+The keys of the map are the categories and the values are the shortnames of the
+representative images.
+
+If you want to remove a category, don't just remove the key, instead set its
+value to ``undefined``.
+
+Due to restrictions intended to prevent addition of undeclared configuration
+settings, it's not possible to add new emoji categories. There is however a
+``custom`` category where you can put your own custom emojis (also known as
+"stickers").
+
+To add custom emojis, you need to edit ``src/headless/emojis.json`` to add new
+entries to the map under the  ``custom`` key.
+
+
+emoji_categories_label
+----------------------
+
+* Default:::
+  {
+    "smileys": "Smileys and emotions",
+    "people": "People",
+    "activity": "Activities",
+    "travel": "Travel",
+    "objects": "Objects",
+    "nature": "Animals and nature",
+    "food": "Food and drink",
+    "symbols": "Symbols",
+    "flags": "Flags",
+    "custom": "Stickers"
+  }
+
+This setting lets you pass in the text value that goes into the `title`
+attribute for the emoji categories. These strings will be translated, but for
+your custom text to be translatable, you'll need to wrap it in `__()``
+somewhere in your own code.
+
 emoji_image_path
 ----------------
 

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 262 - 218
locale/af/LC_MESSAGES/converse.po


+ 100 - 95
sass/_emoji.scss

@@ -7,100 +7,103 @@
             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 {
-                    display: flex;
-                    flex-direction: column;
-                    overflow-y: hidden;
-                    background: white;
-                    .emoji-picker__lists {
-                        height: 100%;
-                        overflow-y: auto;
-                        .emoji-category__heading {
-                            cursor: auto;
-                            color: var(--subdued-color);
-                            font-size: var(--font-size);
-                            padding: 0.5em 0 0 0.5em;
-                        }
+        .sendXMPPMessage {
+            .toggle-smiley {
+                a.toggle-smiley {
+                    padding: 0;
+                }
+                .emoji-picker.toolbar-menu {
+                    min-width: 23rem;
+                    padding-top: 0;
+                    padding-bottom: 0;
+                    background-color: var(--chat-head-color);
+                    .emoji-picker__container {
                         display: flex;
                         flex-direction: column;
-                    }
-                    .emoji-skintone-picker {
-                        display: flex;
-                        label {
-                            margin: 0;
-                            padding: 0 0.5em;
-                            white-space: nowrap;
-                            font-size: var(--font-size-small);
-                            color: var(--heading-color);
+                        overflow-y: hidden;
+                        background: white;
+                        .emoji-picker__lists {
+                            height: 100%;
+                            overflow-y: auto;
+                            .emoji-category__heading {
+                                cursor: auto;
+                                color: var(--subdued-color);
+                                font-size: var(--font-size);
+                                padding: 0.5em 0 0 0.5em;
+                            }
+                            display: flex;
+                            flex-direction: column;
                         }
-                        li {
-                            padding: 0 0.25em;
+                        .emoji-skintone-picker {
+                            display: flex;
+                            label {
+                                margin: 0;
+                                padding: 0 0.5em;
+                                white-space: nowrap;
+                                font-size: var(--font-size-small);
+                                color: var(--heading-color);
+                            }
+                            li {
+                                padding: 0 0.25em;
+                            }
+                            padding: 0.5em 0;
+                            background-color: var(--chat-head-color);
+                            width: auto;
+                            font-size: var(--font-size);
                         }
-                        padding: 0.5em 0;
-                        background-color: var(--chat-head-color);
-                        width: auto;
-                        font-size: var(--font-size);
                     }
-                }
-                .emoji-picker {
-                    background-color: white;
-                    padding: 0.5em;
-                    li {
-                        margin-left: 0;
-                        cursor: pointer;
-                        list-style: none;
-                        position: relative;
-                        &.insert-emoji {
-                            margin: 0;
-                            height: 32px;
-                            width: 32px;
+                    .emoji-picker {
+                        background-color: white;
+                        padding: 0.5em;
+                        li {
+                            margin-left: 0;
+                            cursor: pointer;
+                            list-style: none;
+                            position: relative;
+                            &.insert-emoji {
+                                margin: 0;
+                                height: 32px;
+                                width: 32px;
 
-                            &.picked {
-                                background-color: var(--highlight-color);
-                            }
-                            a {
-                                &:hover {
+                                &.picked {
                                     background-color: var(--highlight-color);
                                 }
-                                font-size: var(--font-size-huge);
+                                a {
+                                    &:hover {
+                                        background-color: var(--highlight-color);
+                                    }
+                                    font-size: var(--font-size-huge);
+                                }
                             }
                         }
                     }
-                }
-                .emoji-picker__header {
-                    display: flex;
-                    flex-direction: column;
-                    padding-top: 0.5em;
-                    background-color: var(--chat-head-color);
-                    .emoji-search {
-                        width: auto;
-                        margin: 0.25em;
-                        height: 2em;
-                        font-size: var(--font-size-small);
-                    }
-                    ul {
+                    .emoji-picker__header {
                         display: flex;
-                        flex-direction: row;
-                        justify-content: space-between;
+                        flex-direction: column;
+                        padding-top: 0.5em;
+                        background-color: var(--chat-head-color);
+                        .emoji-search {
+                            width: auto;
+                            margin: 0.25em;
+                            height: 2em;
+                            font-size: var(--font-size-small);
+                        }
+                        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);
+                            .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);
+                                }
                             }
                         }
                     }
@@ -110,20 +113,22 @@
     }
 
     .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-picker__header {
-                        background-color: var(--chatroom-head-color);
-                        .emoji-category {
-                            &.picked {
-                                border: 1px var(--chatroom-head-color) solid;
-                                border-bottom: none;
+        .sendXMPPMessage {
+            .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-picker__header {
+                            background-color: var(--chatroom-head-color);
+                            .emoji-category {
+                                &.picked {
+                                    border: 1px var(--chatroom-head-color) solid;
+                                    border-bottom: none;
+                                }
                             }
                         }
                     }

+ 1 - 1
spec/emojis.js

@@ -121,7 +121,7 @@
                 await u.waitUntil(() => u.isVisible(view.el.querySelector('.toggle-smiley .emoji-picker__container')));
                 const picker = await u.waitUntil(() => view.el.querySelector('.toggle-smiley .emoji-picker__container'));
                 const input = picker.querySelector('.emoji-search');
-                expect(sizzle('.insert-emoji:not(.hidden)', picker).length).toBe(1589);
+                expect(sizzle('.insert-emoji:not(.hidden)', picker).length).toBe(1591);
 
                 expect(view.emoji_picker_view.model.get('query')).toBeUndefined();
                 input.value = 'smiley';

+ 3 - 2
spec/messages.js

@@ -1026,9 +1026,10 @@
             // Non-https images aren't rendered
             base_url = document.URL.split(window.location.pathname)[0];
             message = base_url+"/logo/conversejs-filled.svg";
-            expect(view.el.querySelectorAll('img').length).toBe(4);
+            const chat_content = view.el.querySelector('.chat-content');
+            expect(chat_content.querySelectorAll('img').length).toBe(4);
             test_utils.sendMessage(view, message);
-            expect(view.el.querySelectorAll('img').length).toBe(4);
+            expect(chat_content.querySelectorAll('img').length).toBe(4);
             done();
         }));
 

+ 22 - 0
src/headless/converse-core.js

@@ -313,6 +313,28 @@ _converse.__ = function (str) {
     return i18n.translate.apply(i18n, arguments);
 };
 
+
+/**
+ * A no-op method which is used to signal to gettext that the passed in string
+ * should be included in the pot translation file.
+ *
+ * In contrast to the double-underscore method, the triple underscore method
+ * doesn't actually translate the strings.
+ *
+ * One reason for this method might be because we're using strings we cannot
+ * send to the translation function because they require variable interpolation
+ * and we don't yet have the variables at scan time.
+ *
+ * @method ___
+ * @private
+ * @memberOf _converse
+ * @param { String } str
+ */
+_converse.___ = function (str) {
+    return str;
+}
+
+
 const __ = _converse.__;
 
 const PROMISES = [

+ 55 - 22
src/headless/converse-emoji.js

@@ -10,7 +10,7 @@ import * as twemoji from "twemoji";
 import _ from "./lodash.noconflict";
 import converse from "./converse-core";
 
-const { Backbone, } = converse.env;
+const { Backbone } = converse.env;
 const u = converse.env.utils;
 
 const ASCII_LIST = {
@@ -166,7 +166,7 @@ converse.plugins.add('converse-emoji', {
          * loaded by converse.js's plugin machinery.
          */
         const { _converse } = this;
-        const { __ } = _converse;
+        const { ___ } = _converse;
 
         _converse.api.settings.update({
             'emoji_image_path': twemoji.default.base,
@@ -179,23 +179,32 @@ converse.plugins.add('converse-emoji', {
                 "nature": ":rainbow:",
                 "food": ":hotdog:",
                 "symbols": ":musical_note:",
-                "flags": ":flag_ac:"
+                "flags": ":flag_ac:",
+                "custom": ":converse:"
+            },
+            // We use the triple-underscore method which doesn't actually
+            // translate but does signify to gettext that these strings should
+            // go into the POT file. The translation then happens in the
+            // template. We do this so that users can pass in their own
+            // strings via converse.initialize, which is before __ is
+            // available.
+            '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"),
+                "custom": ___("Stickers")
             }
         });
+
         _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")
-        }
 
         /**
          * Model for storing data related to the Emoji picker widget
@@ -253,28 +262,43 @@ converse.plugins.add('converse-emoji', {
              */
             getEmojiRenderer () {
                 const how = {
-                    'attributes': (icon) => {
+                    'attributes': icon => {
                         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);
+                const transform = u.shortnamesToEmojis;
+                return _converse.use_system_emojis ? transform : text => twemoji.default.parse(transform(text), how);
             },
 
             /**
-             * Returns unicode represented by the passed in shortname.
-             * @method u.shortnameToUnicode
+             * Returns an emoji represented by the passed in shortname.
+             * Scans the passed in text for shortnames and replaces them with
+             * emoji unicode glyphs or alternatively if it's a custom emoji
+             * without unicode representation then markup for an HTML image tag
+             * is returned.
+             *
+             * The shortname needs to be defined in `emojis.json`
+             * and needs to have either a `cp` attribute for the codepoint, or
+             * an `url` attribute which points to the source for the image.
+             *
+             * @method u.shortnamesToEmojis
              * @param {string} str - String containg the shortname(s)
              */
-            shortnameToUnicode (str) {
+            shortnamesToEmojis (str, unicode_only=false) {
                 str = str.replace(_converse.emojis.shortnames_regex, shortname => {
                     if ((typeof shortname === 'undefined') || (shortname === '') || (!_converse.emoji_shortnames.includes(shortname))) {
                         // if the shortname doesnt exist just return the entire match
                         return shortname;
                     }
-                    const unicode = _converse.emojis_map[shortname].cp.toUpperCase();
-                    return convert(unicode);
+                    const codepoint = _converse.emojis_map[shortname].cp;
+                    if (codepoint) {
+                        return convert(codepoint.toUpperCase());
+                    } else if (unicode_only) {
+                        return shortname;
+                    } else {
+                        return `<img class="emoji" draggable="false" alt="${shortname}" src="${_converse.emojis_map[shortname].url}"/>`;
+                    }
                 });
                 // Also replace ASCII smileys
                 str = str.replace(ASCII_REPLACE_REGEX, (entire, m1, m2, m3) => {
@@ -289,6 +313,15 @@ converse.plugins.add('converse-emoji', {
                 return str;
             },
 
+            /**
+             * Returns unicode represented by the passed in shortname.
+             * @method u.shortnameToUnicode
+             * @param {string} str - String containg the shortname(s)
+             */
+            shortnameToUnicode (str) {
+                return this.shortnamesToEmojis(str, true);
+            },
+
             /**
              * Determines whether the passed in string is just a single emoji shortname;
              * @method u.isSingleEmoji

+ 2 - 11
src/headless/converse-muc.js

@@ -100,8 +100,8 @@ converse.plugins.add('converse-muc', {
         /* The initialize function gets called as soon as the plugin is
          * loaded by converse.js's plugin machinery.
          */
-        const { _converse } = this,
-              { __ } = _converse;
+        const { _converse } = this;
+        const { __, ___ } = _converse;
 
         // Configuration values for this plugin
         // ====================================
@@ -128,15 +128,6 @@ converse.plugins.add('converse-muc', {
         }
 
 
-        function ___ (str) {
-            /* This is part of a hack to get gettext to scan strings to be
-            * translated. Strings we cannot send to the function above because
-            * they require variable interpolation and we don't yet have the
-            * variables at scan time.
-            */
-            return str;
-        }
-
         /* https://xmpp.org/extensions/xep-0045.html
          * ----------------------------------------
          * 100 message      Entering a groupchat         Inform user that any occupant is allowed to see the user's full JID

+ 4 - 1
src/headless/emojis.json

@@ -1,4 +1,8 @@
 {
+"custom": {
+":converse:":{"sn":":converse:","url":"/dist/custom_emojis/converse.png","c":"custom"},
+":xmpp:":{"sn":":xmpp:","url":"/dist/custom_emojis/xmpp.png","c":"custom"}
+},
 "smileys": {
 ":smiley:":{"sn":":smiley:","cp":"1f603","sns":[],"c":"smileys"},
 ":smile:":{"sn":":smile:","cp":"1f604","sns":[],"c":"smileys"},
@@ -2692,4 +2696,3 @@
 ":tone5:":{"sn":":tone5:","cp":"1f3ff","sns":[],"c":"modifier"}
 }
 }
-

+ 15 - 10
src/templates/emojis.html

@@ -4,9 +4,12 @@
         {[ if (!o.query) { ]}
         <ul>
             {[ Object.keys(o.emoji_categories).forEach(function (category) { ]}
-            <li data-category="{{{category}}}" class="emoji-category {{{o.current_category}}} {{{ category}}} {[ if (o.current_category === category) { ]} picked {[ } ]}" title="{{{o._converse.emoji_category_labels[category]}}}">
+                {[ if (o.emoji_categories[category]) { ]}
+                <li data-category="{{{category}}}" class="emoji-category {{{o.current_category}}} {{{ category}}} {[ if (o.current_category === category) { ]} picked {[ } ]}"
+                    title="{{{ o.__(o._converse.emoji_category_labels[category]) }}}">
                     <a class="pick-category" href="#emoji-picker-{{{category}}}" data-category="{{{category}}}"> {{ o.transformCategory(o.emoji_categories[category]) }} </a>
                 </li>
+                {[ } ]}
             {[ }); ]}
         </ul>
         {[ } ]}
@@ -24,15 +27,17 @@
             </ul>
         {[ } else { ]}
             {[ Object.keys(o.emoji_categories).forEach(function (category) { ]}
-                <a id="emoji-picker-{{{category}}}" class="emoji-category__heading" data-category="{{{category}}}">{{{o._converse.emoji_category_labels[category]}}}</a>
-                <ul class="emoji-picker" data-category="{{{category}}}">
-                    {[ Object.values(o.emojis_by_category[category]).forEach(function (emoji) { ]}
-                    <li class="emoji insert-emoji {[ if (o.shouldBeHidden(emoji.sn)) { ]} hidden {[ }; ]}"
-                        data-emoji="{{{emoji.sn}}}" title="{{{emoji.sn}}}">
-                            <a href="#" data-emoji="{{{emoji.sn}}}"> {{ o.transform(emoji.sn) }}  </a>
-                    </li>
-                    {[ }); ]}
-                </ul>
+                {[ if (o.emoji_categories[category]) { ]}
+                    <a id="emoji-picker-{{{category}}}" class="emoji-category__heading" data-category="{{{category}}}">{{{ o.__(o._converse.emoji_category_labels[category]) }}} </a>
+                    <ul class="emoji-picker" data-category="{{{category}}}">
+                        {[ Object.values(o.emojis_by_category[category]).forEach(function (emoji) { ]}
+                        <li class="emoji insert-emoji {[ if (o.shouldBeHidden(emoji.sn)) { ]} hidden {[ }; ]}"
+                            data-emoji="{{{emoji.sn}}}" title="{{{emoji.sn}}}">
+                                <a href="#" data-emoji="{{{emoji.sn}}}"> {{ o.transform(emoji.sn) }}  </a>
+                        </li>
+                        {[ }); ]}
+                    </ul>
+                {[ } ]}
             {[ }); ]}
         {[ } ]}
     </div>

+ 1 - 2
webpack.common.js

@@ -1,6 +1,5 @@
-/* global __dirname, module, process */
+/* global __dirname, module */
 const path = require('path');
-const webpack = require('webpack');
 
 module.exports = {
     output: {

+ 2 - 2
webpack.serve.js

@@ -7,11 +7,11 @@ module.exports = merge(common, {
     mode: "development",
     devtool: "inline-source-map",
     devServer: {
-        contentBase: "./dist"
+        contentBase: "./"
     },
     plugins: [
         new HTMLWebpackPlugin({
-            title: 'Production',
+            title: 'Converse.js Dev',
             template: 'webpack.html'
         })
     ],

이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.