mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	emoji: Add support for translating emoticons.
Add `translate_emoticons` to `prop_types` and `expected_keys`. Furthermore, create a emoji-translating Markdown inline pattern. Also use a JavaScript version of `translate_emoticons` and then use this function during Markdown previews and as a preprocessor. This is only needed for previews, because usually emoticon translation happens on the backend after sending. Add tests for emoticon translation, a settings UI, and a /help/ page as well. Tweaked by tabbott to fix various test failurse as well as how this handles whitespace, requiring emoticons to not have adjacent characters. Fixes #1768.
This commit is contained in:
		
				
					committed by
					
						
						Tim Abbott
					
				
			
			
				
	
			
			
			
						parent
						
							038579b840
						
					
				
				
					commit
					bdb86f1b5e
				
			@@ -413,6 +413,12 @@ var event_fixtures = {
 | 
				
			|||||||
        setting: true,
 | 
					        setting: true,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    update_display_settings__translate_emoticons: {
 | 
				
			||||||
 | 
					        type: 'update_display_settings',
 | 
				
			||||||
 | 
					        setting_name: 'translate_emoticons',
 | 
				
			||||||
 | 
					        setting: true,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    update_global_notifications: {
 | 
					    update_global_notifications: {
 | 
				
			||||||
        type: 'update_global_notifications',
 | 
					        type: 'update_global_notifications',
 | 
				
			||||||
        notification_name: 'enable_stream_sounds',
 | 
					        notification_name: 'enable_stream_sounds',
 | 
				
			||||||
@@ -813,6 +819,10 @@ with_overrides(function (override) {
 | 
				
			|||||||
    dispatch(event);
 | 
					    dispatch(event);
 | 
				
			||||||
    assert_same(page_params.twenty_four_hour_time, true);
 | 
					    assert_same(page_params.twenty_four_hour_time, true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    event = event_fixtures.update_display_settings__translate_emoticons;
 | 
				
			||||||
 | 
					    page_params.translate_emoticons = false;
 | 
				
			||||||
 | 
					    dispatch(event);
 | 
				
			||||||
 | 
					    assert_same(page_params.translate_emoticons, true);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
with_overrides(function (override) {
 | 
					with_overrides(function (override) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,8 @@ set_global('upload_widget', {});
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
zrequire('emoji_codes', 'generated/emoji/emoji_codes');
 | 
					zrequire('emoji_codes', 'generated/emoji/emoji_codes');
 | 
				
			||||||
zrequire('emoji');
 | 
					zrequire('emoji');
 | 
				
			||||||
 | 
					zrequire('markdown');
 | 
				
			||||||
 | 
					zrequire('util');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
(function test_build_emoji_upload_widget() {
 | 
					(function test_build_emoji_upload_widget() {
 | 
				
			||||||
    var build_widget_stub = false;
 | 
					    var build_widget_stub = false;
 | 
				
			||||||
@@ -74,3 +76,17 @@ zrequire('emoji');
 | 
				
			|||||||
    emoji.get_canonical_name('non_existent');
 | 
					    emoji.get_canonical_name('non_existent');
 | 
				
			||||||
    assert(errored);
 | 
					    assert(errored);
 | 
				
			||||||
}());
 | 
					}());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					(function test_translate_emoticons_to_names() {
 | 
				
			||||||
 | 
					  global.emoji_codes = {
 | 
				
			||||||
 | 
					      codepoint_to_name: {
 | 
				
			||||||
 | 
					          '1f603': 'smiley',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  var test_text = 'Testing :)';
 | 
				
			||||||
 | 
					  var expected = 'Testing :smiley:';
 | 
				
			||||||
 | 
					  var result = emoji.translate_emoticons_to_names(test_text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  assert.equal(expected, result);
 | 
				
			||||||
 | 
					}());
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,6 +41,7 @@ set_global('page_params', {
 | 
				
			|||||||
            "https://zone_%(zone)s.zulip.net/ticket/%(id)s",
 | 
					            "https://zone_%(zone)s.zulip.net/ticket/%(id)s",
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
 | 
					    translate_emoticons: false,
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
set_global('blueslip', {error: function () {}});
 | 
					set_global('blueslip', {error: function () {}});
 | 
				
			||||||
@@ -304,6 +305,17 @@ var bugdown_data = JSON.parse(fs.readFileSync(path.join(__dirname, '../../zerver
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        assert.equal(expected, output);
 | 
					        assert.equal(expected, output);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Here to arrange 100% test coverage for the new emoticons code
 | 
				
			||||||
 | 
					    // path.  TODO: Have a better way to test this setting in both
 | 
				
			||||||
 | 
					    // states, once we implement the local echo feature properly.
 | 
				
			||||||
 | 
					    // Probably a good technique would be to support toggling the
 | 
				
			||||||
 | 
					    // page_params setting inside the `test_cases.forEach` loop above.
 | 
				
			||||||
 | 
					    page_params.translate_emoticons = true;
 | 
				
			||||||
 | 
					    var message = {raw_content: ":)"};
 | 
				
			||||||
 | 
					    markdown.apply_markdown(message);
 | 
				
			||||||
 | 
					    assert.equal('<p><span class="emoji emoji-1f603" title="smiley">:smiley:</span></p>', message.content);
 | 
				
			||||||
 | 
					    page_params.translate_emoticons = false;
 | 
				
			||||||
}());
 | 
					}());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
(function test_subject_links() {
 | 
					(function test_subject_links() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -885,6 +885,7 @@ exports.initialize = function () {
 | 
				
			|||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            preview_html = rendered_content;
 | 
					            preview_html = rendered_content;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $("#preview_content").html(preview_html);
 | 
					        $("#preview_content").html(preview_html);
 | 
				
			||||||
        if (page_params.emojiset === "text") {
 | 
					        if (page_params.emojiset === "text") {
 | 
				
			||||||
            $("#preview_content").find(".emoji").replaceWith(function () {
 | 
					            $("#preview_content").find(".emoji").replaceWith(function () {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,6 +17,18 @@ var zulip_emoji = {
 | 
				
			|||||||
    deactivated: false,
 | 
					    deactivated: false,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Emoticons, and which emoji they should become (without colons). Duplicate
 | 
				
			||||||
 | 
					// emoji are allowed. Changes here should be mimicked in `zerver/lib/emoji.py`
 | 
				
			||||||
 | 
					// and `templates/zerver/help/enable-emoticon-translations.md`.
 | 
				
			||||||
 | 
					var EMOTICON_CONVERSIONS = {
 | 
				
			||||||
 | 
					    ':)': 'smiley',
 | 
				
			||||||
 | 
					    '(:': 'smiley',
 | 
				
			||||||
 | 
					    ':(': 'slightly_frowning_face',
 | 
				
			||||||
 | 
					    '<3': 'heart',
 | 
				
			||||||
 | 
					    ':|': 'expressionless',
 | 
				
			||||||
 | 
					    ':/': 'confused',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports.update_emojis = function update_emojis(realm_emojis) {
 | 
					exports.update_emojis = function update_emojis(realm_emojis) {
 | 
				
			||||||
    // exports.all_realm_emojis is emptied before adding the realm-specific emoji to it.
 | 
					    // exports.all_realm_emojis is emptied before adding the realm-specific emoji to it.
 | 
				
			||||||
    // This makes sure that in case of deletion, the deleted realm_emojis don't
 | 
					    // This makes sure that in case of deletion, the deleted realm_emojis don't
 | 
				
			||||||
@@ -105,6 +117,22 @@ exports.get_canonical_name = function (emoji_name) {
 | 
				
			|||||||
    return emoji_codes.codepoint_to_name[codepoint];
 | 
					    return emoji_codes.codepoint_to_name[codepoint];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Translates emoticons in a string to their colon syntax.
 | 
				
			||||||
 | 
					exports.translate_emoticons_to_names = function translate_emoticons_to_names(text) {
 | 
				
			||||||
 | 
					    var translated = text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for (var emoticon in EMOTICON_CONVERSIONS) {
 | 
				
			||||||
 | 
					        if (EMOTICON_CONVERSIONS.hasOwnProperty(emoticon)) {
 | 
				
			||||||
 | 
					            var emoticon_reg_ex = new RegExp(util.escape_regexp(emoticon), "g");
 | 
				
			||||||
 | 
					            translated = translated.replace(
 | 
				
			||||||
 | 
					                emoticon_reg_ex,
 | 
				
			||||||
 | 
					                ':' + EMOTICON_CONVERSIONS[emoticon] + ':');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return translated;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
return exports;
 | 
					return exports;
 | 
				
			||||||
}());
 | 
					}());
 | 
				
			||||||
if (typeof module !== 'undefined') {
 | 
					if (typeof module !== 'undefined') {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -358,6 +358,16 @@ exports.initialize = function () {
 | 
				
			|||||||
        return fenced_code.process_fenced_code(src);
 | 
					        return fenced_code.process_fenced_code(src);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    function preprocess_translate_emoticons(src) {
 | 
				
			||||||
 | 
					        if (!page_params.translate_emoticons) {
 | 
				
			||||||
 | 
					            return src;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // In this scenario, the message has to be from the user, so the only
 | 
				
			||||||
 | 
					        // requirement should be that they have the setting on.
 | 
				
			||||||
 | 
					        return emoji.translate_emoticons_to_names(src);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Disable ordered lists
 | 
					    // Disable ordered lists
 | 
				
			||||||
    // We used GFM + tables, so replace the list start regex for that ruleset
 | 
					    // We used GFM + tables, so replace the list start regex for that ruleset
 | 
				
			||||||
    // We remove the |[\d+]\. that matches the numbering in a numbered list
 | 
					    // We remove the |[\d+]\. that matches the numbering in a numbered list
 | 
				
			||||||
@@ -406,7 +416,11 @@ exports.initialize = function () {
 | 
				
			|||||||
        realmFilterHandler: handleRealmFilter,
 | 
					        realmFilterHandler: handleRealmFilter,
 | 
				
			||||||
        texHandler: handleTex,
 | 
					        texHandler: handleTex,
 | 
				
			||||||
        renderer: r,
 | 
					        renderer: r,
 | 
				
			||||||
        preprocessors: [preprocess_code_blocks, preprocess_auto_olists],
 | 
					        preprocessors: [
 | 
				
			||||||
 | 
					            preprocess_code_blocks,
 | 
				
			||||||
 | 
					            preprocess_auto_olists,
 | 
				
			||||||
 | 
					            preprocess_translate_emoticons,
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -288,6 +288,7 @@ exports.dispatch_normal_event = function dispatch_normal_event(event) {
 | 
				
			|||||||
            'left_side_userlist',
 | 
					            'left_side_userlist',
 | 
				
			||||||
            'timezone',
 | 
					            'timezone',
 | 
				
			||||||
            'twenty_four_hour_time',
 | 
					            'twenty_four_hour_time',
 | 
				
			||||||
 | 
					            'translate_emoticons',
 | 
				
			||||||
        ];
 | 
					        ];
 | 
				
			||||||
        if (_.contains(user_display_settings, event.setting_name)) {
 | 
					        if (_.contains(user_display_settings, event.setting_name)) {
 | 
				
			||||||
            page_params[event.setting_name] = event.setting;
 | 
					            page_params[event.setting_name] = event.setting;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -184,6 +184,31 @@ exports.set_up = function () {
 | 
				
			|||||||
            },
 | 
					            },
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    $("#translate_emoticons").change(function () {
 | 
				
			||||||
 | 
					        var data = {};
 | 
				
			||||||
 | 
					        var setting_value = $("#translate_emoticons").is(":checked");
 | 
				
			||||||
 | 
					        data.translate_emoticons = JSON.stringify(setting_value);
 | 
				
			||||||
 | 
					        var context = {};
 | 
				
			||||||
 | 
					        if (data.translate_emoticons === "true") {
 | 
				
			||||||
 | 
					            context.new_mode = i18n.t("be");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            context.new_mode = i18n.t("not be");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        channel.patch({
 | 
				
			||||||
 | 
					            url: '/json/settings/display',
 | 
				
			||||||
 | 
					            data: data,
 | 
				
			||||||
 | 
					            success: function () {
 | 
				
			||||||
 | 
					                ui_report.success(i18n.t("Emoticons will now __new_mode__ translated!", context),
 | 
				
			||||||
 | 
					                                  $('#display-settings-status').expectOne());
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            error: function (xhr) {
 | 
				
			||||||
 | 
					                ui_report.error(i18n.t("Error updating emoticon translation setting"), xhr,
 | 
				
			||||||
 | 
					                                $('#display-settings-status').expectOne());
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports.report_emojiset_change = function () {
 | 
					exports.report_emojiset_change = function () {
 | 
				
			||||||
@@ -214,6 +239,7 @@ function _update_page() {
 | 
				
			|||||||
    $("#twenty_four_hour_time").prop('checked', page_params.twenty_four_hour_time);
 | 
					    $("#twenty_four_hour_time").prop('checked', page_params.twenty_four_hour_time);
 | 
				
			||||||
    $("#left_side_userlist").prop('checked', page_params.left_side_userlist);
 | 
					    $("#left_side_userlist").prop('checked', page_params.left_side_userlist);
 | 
				
			||||||
    $("#default_language_name").text(page_params.default_language_name);
 | 
					    $("#default_language_name").text(page_params.default_language_name);
 | 
				
			||||||
 | 
					    $("#translate_emoticons").prop('checked', page_params.translate_emoticons);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exports.update_page = function () {
 | 
					exports.update_page = function () {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -78,7 +78,21 @@
 | 
				
			|||||||
            </select>
 | 
					            </select>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <h3>Emoji style</h3>
 | 
					        <h3 class="light">Emoji style</h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        <div class="input-group side-padded-container">
 | 
				
			||||||
 | 
					            <label class="checkbox">
 | 
				
			||||||
 | 
					                <input type="checkbox" name="translate_emoticons" id="translate_emoticons"
 | 
				
			||||||
 | 
					                       {{#if page_params.translate_emoticons}}
 | 
				
			||||||
 | 
					                       checked="checked"
 | 
				
			||||||
 | 
					                       {{/if}} />
 | 
				
			||||||
 | 
					                <span></span>
 | 
				
			||||||
 | 
					            </label>
 | 
				
			||||||
 | 
					            <label for="translate_emoticons" class="inline-block">
 | 
				
			||||||
 | 
					                {{t "Translate emoticons (convert <code>:)</code> to 😃 in messages)" }}
 | 
				
			||||||
 | 
					            </label>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <div class="input-group side-padded-container">
 | 
					        <div class="input-group side-padded-container">
 | 
				
			||||||
            <div class="emojiset_choices grey-box">
 | 
					            <div class="emojiset_choices grey-box">
 | 
				
			||||||
                {{#each page_params.emojiset_choices }}
 | 
					                {{#each page_params.emojiset_choices }}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										92
									
								
								templates/zerver/help/enable-emoticon-translations.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								templates/zerver/help/enable-emoticon-translations.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,92 @@
 | 
				
			|||||||
 | 
					# Enable emoticon translation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you use emoticons like `:)` or `:/`, you can have them translated into
 | 
				
			||||||
 | 
					emoji equivalents like
 | 
				
			||||||
 | 
					<img
 | 
				
			||||||
 | 
					    src="/static/generated/emoji/images/emoji/smile.png"
 | 
				
			||||||
 | 
					    alt="smiley"
 | 
				
			||||||
 | 
					    style="width: 3%;"
 | 
				
			||||||
 | 
					/>
 | 
				
			||||||
 | 
					or
 | 
				
			||||||
 | 
					<img
 | 
				
			||||||
 | 
					    src="/static/generated/emoji/images/emoji/slightly_frowning_face.png"
 | 
				
			||||||
 | 
					    alt="slightly_frowning_face"
 | 
				
			||||||
 | 
					    style="width: 3%;"
 | 
				
			||||||
 | 
					/>
 | 
				
			||||||
 | 
					automatically by Zulip.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{!go-to-the.md!} [Display settings](/#settings/display-settings)
 | 
				
			||||||
 | 
					{!settings.md!}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					2. Select the option labeled
 | 
				
			||||||
 | 
					   **Translate emoticons (convert `:)` to 😃 in messages)**.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Then whenever you send a message with a supported emoticon, it will be
 | 
				
			||||||
 | 
					translated into an emoji.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Current emoticon conversions
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<table>
 | 
				
			||||||
 | 
					    <thead>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					            <th align="center">Emoticon</th>
 | 
				
			||||||
 | 
					            <th align="center">Emoji</th>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					    </thead>
 | 
				
			||||||
 | 
					    <tbody>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					            <td align="center"><code>:)</code></td>
 | 
				
			||||||
 | 
					            <td align="center">
 | 
				
			||||||
 | 
					                <img
 | 
				
			||||||
 | 
					                    src="/static/generated/emoji/images/emoji/smiley.png"
 | 
				
			||||||
 | 
					                    alt="smiley"
 | 
				
			||||||
 | 
					                    style="width: 30%;">
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					            <td align="center"><code>(:</code></td>
 | 
				
			||||||
 | 
					            <td align="center">
 | 
				
			||||||
 | 
					                <img
 | 
				
			||||||
 | 
					                    src="/static/generated/emoji/images/emoji/smiley.png"
 | 
				
			||||||
 | 
					                    alt="smiley"
 | 
				
			||||||
 | 
					                    style="width: 30%;">
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					            <td align="center"><code>:(</code></td>
 | 
				
			||||||
 | 
					            <td align="center">
 | 
				
			||||||
 | 
					                <img
 | 
				
			||||||
 | 
					                    src="/static/generated/emoji/images/emoji/slightly_frowning_face.png"
 | 
				
			||||||
 | 
					                    alt="slightly_frowning_face"
 | 
				
			||||||
 | 
					                    style="width: 30%;">
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					            <td align="center"><code><3</code></td>
 | 
				
			||||||
 | 
					            <td align="center">
 | 
				
			||||||
 | 
					                <img
 | 
				
			||||||
 | 
					                    src="/static/generated/emoji/images/emoji/heart.png"
 | 
				
			||||||
 | 
					                    alt="heart"
 | 
				
			||||||
 | 
					                    style="width: 30%;">
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					            <td align="center"><code>:|</code></td>
 | 
				
			||||||
 | 
					            <td align="center">
 | 
				
			||||||
 | 
					                <img
 | 
				
			||||||
 | 
					                    src="/static/generated/emoji/images/emoji/expressionless.png"
 | 
				
			||||||
 | 
					                    alt="expressionless"
 | 
				
			||||||
 | 
					                    style="width: 30%;">
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					        <tr>
 | 
				
			||||||
 | 
					            <td align="center"><code>:/</code></td>
 | 
				
			||||||
 | 
					            <td align="center">
 | 
				
			||||||
 | 
					                <img
 | 
				
			||||||
 | 
					                    src="/static/generated/emoji/images/emoji/confused.png"
 | 
				
			||||||
 | 
					                    alt="confused"
 | 
				
			||||||
 | 
					                    style="width: 30%;">
 | 
				
			||||||
 | 
					            </td>
 | 
				
			||||||
 | 
					        </tr>
 | 
				
			||||||
 | 
					    </tbody>
 | 
				
			||||||
 | 
					</table>
 | 
				
			||||||
@@ -99,6 +99,7 @@
 | 
				
			|||||||
* [Bots and integrations](/help/add-a-bot-or-integration)
 | 
					* [Bots and integrations](/help/add-a-bot-or-integration)
 | 
				
			||||||
* [Enable high contrast mode](/help/enable-high-contrast-mode)
 | 
					* [Enable high contrast mode](/help/enable-high-contrast-mode)
 | 
				
			||||||
* [Enable night mode](/help/enable-night-mode)
 | 
					* [Enable night mode](/help/enable-night-mode)
 | 
				
			||||||
 | 
					* [Enable emoticon translations](/help/enable-emoticon-translations)
 | 
				
			||||||
* [Display the buddy list on narrow screens](/help/move-the-users-list-to-the-left-sidebar)
 | 
					* [Display the buddy list on narrow screens](/help/move-the-users-list-to-the-left-sidebar)
 | 
				
			||||||
* [View organization statistics](/help/analytics)
 | 
					* [View organization statistics](/help/analytics)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -336,9 +336,28 @@
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      "name": "many_emoji",
 | 
					      "name": "many_emoji",
 | 
				
			||||||
      "input":  "test :smile: again :poop:\n:) foo:)bar x::y::z :wasted waste: :fakeemojithisshouldnotrender:",
 | 
					      "input":  "test :smile: again :poop:\n foobar x::y::z :wasted waste: :fakeemojithisshouldnotrender:",
 | 
				
			||||||
      "expected_output": "<p>test <span class=\"emoji emoji-1f604\" title=\"smile\">:smile:</span> again <span class=\"emoji emoji-1f4a9\" title=\"poop\">:poop:</span><br>\n:) foo:)bar x::y::z :wasted waste: :fakeemojithisshouldnotrender:</p>",
 | 
					      "expected_output": "<p>test <span class=\"emoji emoji-1f604\" title=\"smile\">:smile:</span> again <span class=\"emoji emoji-1f4a9\" title=\"poop\">:poop:</span><br>\n foobar x::y::z :wasted waste: :fakeemojithisshouldnotrender:</p>",
 | 
				
			||||||
      "text_content": "test \ud83d\ude04 again \ud83d\udca9\n:) foo:)bar x::y::z :wasted waste: :fakeemojithisshouldnotrender:"
 | 
					      "text_content": "test \ud83d\ude04 again \ud83d\udca9\n foobar x::y::z :wasted waste: :fakeemojithisshouldnotrender:"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "name": "translate_emoticons",
 | 
				
			||||||
 | 
					      "input":  ":) foo :( bar <3 with space : ) real emoji :smiley:",
 | 
				
			||||||
 | 
					      "expected_output": "<p><span class=\"emoji emoji-1f603\" title=\"smiley\">:smiley:</span> foo <span class=\"emoji emoji-1f641\" title=\"slightly frowning face\">:slightly_frowning_face:</span> bar <span class=\"emoji emoji-2764\" title=\"heart\">:heart:</span> with space : ) real emoji <span class=\"emoji emoji-1f603\" title=\"smiley\">:smiley:</span></p>",
 | 
				
			||||||
 | 
					      "marked_expected_output": "<p>:) foo :( bar <3 with space : ) real emoji <span class=\"emoji emoji-1f603\" title=\"smiley\">:smiley:</span></p>",
 | 
				
			||||||
 | 
					      "text_content": "\ud83d\ude03 foo \ud83d\ude41 bar \u2764 with space : ) real emoji \ud83d\ude03"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "name": "translate_emoticons_whitepsace",
 | 
				
			||||||
 | 
					      "input":  "a:) ;)b",
 | 
				
			||||||
 | 
					      "expected_output": "<p>a:) ;)b</p>",
 | 
				
			||||||
 | 
					      "text_content": "a:) ;)b"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					      "name": "translate_emoticons_in_code",
 | 
				
			||||||
 | 
					      "input":  "`:)`",
 | 
				
			||||||
 | 
					      "expected_output": "<p><code>:)</code></p>",
 | 
				
			||||||
 | 
					      "text_content": ":)"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      "name": "random_emoji_1",
 | 
					      "name": "random_emoji_1",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -32,6 +32,7 @@ from markdown.extensions import codehilite
 | 
				
			|||||||
from zerver.lib.bugdown import fenced_code
 | 
					from zerver.lib.bugdown import fenced_code
 | 
				
			||||||
from zerver.lib.bugdown.fenced_code import FENCE_RE
 | 
					from zerver.lib.bugdown.fenced_code import FENCE_RE
 | 
				
			||||||
from zerver.lib.camo import get_camo_url
 | 
					from zerver.lib.camo import get_camo_url
 | 
				
			||||||
 | 
					from zerver.lib.emoji import translate_emoticons, emoticon_regex
 | 
				
			||||||
from zerver.lib.mention import possible_mentions, \
 | 
					from zerver.lib.mention import possible_mentions, \
 | 
				
			||||||
    possible_user_group_mentions, extract_user_group
 | 
					    possible_user_group_mentions, extract_user_group
 | 
				
			||||||
from zerver.lib.notifications import encode_stream
 | 
					from zerver.lib.notifications import encode_stream
 | 
				
			||||||
@@ -998,6 +999,19 @@ def unicode_emoji_to_codepoint(unicode_emoji: Text) -> Text:
 | 
				
			|||||||
        codepoint = '0' + codepoint
 | 
					        codepoint = '0' + codepoint
 | 
				
			||||||
    return codepoint
 | 
					    return codepoint
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class EmoticonTranslation(markdown.inlinepatterns.Pattern):
 | 
				
			||||||
 | 
					    """ Translates emoticons like `:)` into emoji like `:smile:`. """
 | 
				
			||||||
 | 
					    def handleMatch(self, match: Match[Text]) -> Optional[Element]:
 | 
				
			||||||
 | 
					        # If there is `db_data` and it is false, then don't do translating.
 | 
				
			||||||
 | 
					        # If there is no `db_data`, such as during tests, translate.
 | 
				
			||||||
 | 
					        if db_data is not None and not db_data['translate_emoticons']:
 | 
				
			||||||
 | 
					            return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        emoticon = match.group('emoticon')
 | 
				
			||||||
 | 
					        translated = translate_emoticons(emoticon)
 | 
				
			||||||
 | 
					        name = translated[1:-1]
 | 
				
			||||||
 | 
					        return make_emoji(name_to_codepoint[name], translated)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class UnicodeEmoji(markdown.inlinepatterns.Pattern):
 | 
					class UnicodeEmoji(markdown.inlinepatterns.Pattern):
 | 
				
			||||||
    def handleMatch(self, match: Match[Text]) -> Optional[Element]:
 | 
					    def handleMatch(self, match: Match[Text]) -> Optional[Element]:
 | 
				
			||||||
        orig_syntax = match.group('syntax')
 | 
					        orig_syntax = match.group('syntax')
 | 
				
			||||||
@@ -1578,6 +1592,7 @@ class Bugdown(markdown.Extension):
 | 
				
			|||||||
            Tex(r'\B(?<!\$)\$\$(?P<body>[^\n_$](\\\$|[^$\n])*)\$\$(?!\$)\B'),
 | 
					            Tex(r'\B(?<!\$)\$\$(?P<body>[^\n_$](\\\$|[^$\n])*)\$\$(?!\$)\B'),
 | 
				
			||||||
            '>backtick')
 | 
					            '>backtick')
 | 
				
			||||||
        md.inlinePatterns.add('emoji', Emoji(EMOJI_REGEX), '_end')
 | 
					        md.inlinePatterns.add('emoji', Emoji(EMOJI_REGEX), '_end')
 | 
				
			||||||
 | 
					        md.inlinePatterns.add('translate_emoticons', EmoticonTranslation(emoticon_regex), '>emoji')
 | 
				
			||||||
        md.inlinePatterns.add('unicodeemoji', UnicodeEmoji(unicode_emoji_regex), '_end')
 | 
					        md.inlinePatterns.add('unicodeemoji', UnicodeEmoji(unicode_emoji_regex), '_end')
 | 
				
			||||||
        md.inlinePatterns.add('link', AtomicLinkPattern(markdown.inlinepatterns.LINK_RE, md), '>avatar')
 | 
					        md.inlinePatterns.add('link', AtomicLinkPattern(markdown.inlinepatterns.LINK_RE, md), '>avatar')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -1956,6 +1971,7 @@ def do_convert(content: Text,
 | 
				
			|||||||
            'realm_uri': message_realm.uri,
 | 
					            'realm_uri': message_realm.uri,
 | 
				
			||||||
            'sent_by_bot': sent_by_bot,
 | 
					            'sent_by_bot': sent_by_bot,
 | 
				
			||||||
            'stream_names': stream_name_info,
 | 
					            'stream_names': stream_name_info,
 | 
				
			||||||
 | 
					            'translate_emoticons': message.sender.translate_emoticons,
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try:
 | 
					    try:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,31 @@ from zerver.models import Reaction, Realm, RealmEmoji, UserProfile
 | 
				
			|||||||
NAME_TO_CODEPOINT_PATH = os.path.join(settings.STATIC_ROOT, "generated", "emoji", "name_to_codepoint.json")
 | 
					NAME_TO_CODEPOINT_PATH = os.path.join(settings.STATIC_ROOT, "generated", "emoji", "name_to_codepoint.json")
 | 
				
			||||||
CODEPOINT_TO_NAME_PATH = os.path.join(settings.STATIC_ROOT, "generated", "emoji", "codepoint_to_name.json")
 | 
					CODEPOINT_TO_NAME_PATH = os.path.join(settings.STATIC_ROOT, "generated", "emoji", "codepoint_to_name.json")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Emoticons and which emoji they should become. Duplicate emoji are allowed.
 | 
				
			||||||
 | 
					# Changes here should be mimicked in `static/js/emoji.js`
 | 
				
			||||||
 | 
					# and `templates/zerver/help/enable-emoticon-translations.md`.
 | 
				
			||||||
 | 
					EMOTICON_CONVERSIONS = {
 | 
				
			||||||
 | 
					    ':)': ':smiley:',
 | 
				
			||||||
 | 
					    '(:': ':smiley:',
 | 
				
			||||||
 | 
					    ':(': ':slightly_frowning_face:',
 | 
				
			||||||
 | 
					    '<3': ':heart:',
 | 
				
			||||||
 | 
					    ':|': ':expressionless:',
 | 
				
			||||||
 | 
					    ':/': ':confused:',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					possible_emoticons = EMOTICON_CONVERSIONS.keys()
 | 
				
			||||||
 | 
					possible_emoticon_regexes = map(re.escape, possible_emoticons)  # type: ignore # AnyStr/str issues
 | 
				
			||||||
 | 
					emoticon_regex = '(?<![^\s])(?P<emoticon>(' + ')|('.join(possible_emoticon_regexes) + '))(?![\S])'  # type: ignore # annoying
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Translates emoticons to their colon syntax, e.g. `:smiley:`.
 | 
				
			||||||
 | 
					def translate_emoticons(text: Text) -> Text:
 | 
				
			||||||
 | 
					    translated = text
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for emoticon in EMOTICON_CONVERSIONS:
 | 
				
			||||||
 | 
					        translated = re.sub(re.escape(emoticon), EMOTICON_CONVERSIONS[emoticon], translated)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return translated
 | 
				
			||||||
 | 
					
 | 
				
			||||||
with open(NAME_TO_CODEPOINT_PATH) as fp:
 | 
					with open(NAME_TO_CODEPOINT_PATH) as fp:
 | 
				
			||||||
    name_to_codepoint = ujson.load(fp)
 | 
					    name_to_codepoint = ujson.load(fp)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										20
									
								
								zerver/migrations/0142_userprofile_translate_emoticons.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								zerver/migrations/0142_userprofile_translate_emoticons.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
				
			|||||||
 | 
					# -*- coding: utf-8 -*-
 | 
				
			||||||
 | 
					# Generated by Django 1.11.6 on 2018-02-19 22:27
 | 
				
			||||||
 | 
					from __future__ import unicode_literals
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from django.db import migrations, models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    dependencies = [
 | 
				
			||||||
 | 
					        ('zerver', '0141_change_usergroup_description_to_textfield'),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    operations = [
 | 
				
			||||||
 | 
					        migrations.AddField(
 | 
				
			||||||
 | 
					            model_name='userprofile',
 | 
				
			||||||
 | 
					            name='translate_emoticons',
 | 
				
			||||||
 | 
					            field=models.BooleanField(default=False),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@@ -588,6 +588,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin):
 | 
				
			|||||||
    default_language = models.CharField(default=u'en', max_length=MAX_LANGUAGE_ID_LENGTH)  # type: Text
 | 
					    default_language = models.CharField(default=u'en', max_length=MAX_LANGUAGE_ID_LENGTH)  # type: Text
 | 
				
			||||||
    high_contrast_mode = models.BooleanField(default=False)  # type: bool
 | 
					    high_contrast_mode = models.BooleanField(default=False)  # type: bool
 | 
				
			||||||
    night_mode = models.BooleanField(default=False)  # type: bool
 | 
					    night_mode = models.BooleanField(default=False)  # type: bool
 | 
				
			||||||
 | 
					    translate_emoticons = models.BooleanField(default=False)  # type: bool
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # Hours to wait before sending another email to a user
 | 
					    # Hours to wait before sending another email to a user
 | 
				
			||||||
    EMAIL_REMINDER_WAITPERIOD = 24
 | 
					    EMAIL_REMINDER_WAITPERIOD = 24
 | 
				
			||||||
@@ -650,6 +651,7 @@ class UserProfile(AbstractBaseUser, PermissionsMixin):
 | 
				
			|||||||
        twenty_four_hour_time=bool,
 | 
					        twenty_four_hour_time=bool,
 | 
				
			||||||
        high_contrast_mode=bool,
 | 
					        high_contrast_mode=bool,
 | 
				
			||||||
        night_mode=bool,
 | 
					        night_mode=bool,
 | 
				
			||||||
 | 
					        translate_emoticons=bool,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    notification_setting_types = dict(
 | 
					    notification_setting_types = dict(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ from django.test import TestCase, override_settings
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from zerver.lib import bugdown
 | 
					from zerver.lib import bugdown
 | 
				
			||||||
from zerver.lib.actions import (
 | 
					from zerver.lib.actions import (
 | 
				
			||||||
 | 
					    do_set_user_display_setting,
 | 
				
			||||||
    do_remove_realm_emoji,
 | 
					    do_remove_realm_emoji,
 | 
				
			||||||
    do_set_alert_words,
 | 
					    do_set_alert_words,
 | 
				
			||||||
    get_realm,
 | 
					    get_realm,
 | 
				
			||||||
@@ -617,6 +618,16 @@ class BugdownTest(ZulipTestCase):
 | 
				
			|||||||
        converted = bugdown_convert(msg)
 | 
					        converted = bugdown_convert(msg)
 | 
				
			||||||
        self.assertEqual(converted, u'<p><span class="emoji emoji-2615" title="coffee">:coffee:</span><span class="emoji emoji-2615" title="coffee">:coffee:</span></p>')
 | 
					        self.assertEqual(converted, u'<p><span class="emoji emoji-2615" title="coffee">:coffee:</span><span class="emoji emoji-2615" title="coffee">:coffee:</span></p>')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_no_translate_emoticons_if_off(self) -> None:
 | 
				
			||||||
 | 
					        user_profile = self.example_user('othello')
 | 
				
			||||||
 | 
					        do_set_user_display_setting(user_profile, 'translate_emoticons', False)
 | 
				
			||||||
 | 
					        msg = Message(sender=user_profile, sending_client=get_client("test"))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        content = u':)'
 | 
				
			||||||
 | 
					        expected = u'<p>:)</p>'
 | 
				
			||||||
 | 
					        converted = render_markdown(msg, content)
 | 
				
			||||||
 | 
					        self.assertEqual(converted, expected)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_same_markup(self) -> None:
 | 
					    def test_same_markup(self) -> None:
 | 
				
			||||||
        msg = u'\u2615'  # ☕
 | 
					        msg = u'\u2615'  # ☕
 | 
				
			||||||
        unicode_converted = bugdown_convert(msg)
 | 
					        unicode_converted = bugdown_convert(msg)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -156,6 +156,7 @@ class HomeTest(ZulipTestCase):
 | 
				
			|||||||
            "subscriptions",
 | 
					            "subscriptions",
 | 
				
			||||||
            "test_suite",
 | 
					            "test_suite",
 | 
				
			||||||
            "timezone",
 | 
					            "timezone",
 | 
				
			||||||
 | 
					            "translate_emoticons",
 | 
				
			||||||
            "twenty_four_hour_time",
 | 
					            "twenty_four_hour_time",
 | 
				
			||||||
            "unread_msgs",
 | 
					            "unread_msgs",
 | 
				
			||||||
            "unsubscribed",
 | 
					            "unsubscribed",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -129,6 +129,7 @@ def update_display_settings_backend(
 | 
				
			|||||||
        twenty_four_hour_time: Optional[bool]=REQ(validator=check_bool, default=None),
 | 
					        twenty_four_hour_time: Optional[bool]=REQ(validator=check_bool, default=None),
 | 
				
			||||||
        high_contrast_mode: Optional[bool]=REQ(validator=check_bool, default=None),
 | 
					        high_contrast_mode: Optional[bool]=REQ(validator=check_bool, default=None),
 | 
				
			||||||
        night_mode: Optional[bool]=REQ(validator=check_bool, default=None),
 | 
					        night_mode: Optional[bool]=REQ(validator=check_bool, default=None),
 | 
				
			||||||
 | 
					        translate_emoticons: Optional[bool]=REQ(validator=check_bool, default=None),
 | 
				
			||||||
        default_language: Optional[bool]=REQ(validator=check_string, default=None),
 | 
					        default_language: Optional[bool]=REQ(validator=check_string, default=None),
 | 
				
			||||||
        left_side_userlist: Optional[bool]=REQ(validator=check_bool, default=None),
 | 
					        left_side_userlist: Optional[bool]=REQ(validator=check_bool, default=None),
 | 
				
			||||||
        emojiset: Optional[str]=REQ(validator=check_string, default=None),
 | 
					        emojiset: Optional[str]=REQ(validator=check_string, default=None),
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user