Commit e6fc0207 authored by Eric Eastwood's avatar Eric Eastwood

Use native unicode emojis

 - gl_emoji for falling back to image/css-sprite when the browser
   doesn't support an emoji
 - Markdown rendering (Banzai filter)
 - Autocomplete
 - Award emoji menu
    - Perceived perf
    - Immediate response because we now build client-side
 - Update `digests.json` generation in gemojione rake task to be more
   useful and  include `unicodeVersion`

MR: !9437

See issues

 - #26371
 - #27250
 - #22474
parent f911b948
app/assets/images/emoji.png

1.04 MB | W: | H:

app/assets/images/emoji.png

1.16 MB | W: | H:

app/assets/images/emoji.png
app/assets/images/emoji.png
app/assets/images/emoji.png
app/assets/images/emoji.png
  • 2-up
  • Swipe
  • Onion skin
app/assets/images/emoji@2x.png

2.53 MB | W: | H:

app/assets/images/emoji@2x.png

2.84 MB | W: | H:

app/assets/images/emoji@2x.png
app/assets/images/emoji@2x.png
app/assets/images/emoji@2x.png
app/assets/images/emoji@2x.png
  • 2-up
  • Swipe
  • Onion skin
This diff is collapsed.
const installCustomElements = require('document-register-element');
const emojiMap = require('emoji-map');
const emojiAliases = require('emoji-aliases');
const generatedUnicodeSupportMap = require('./gl_emoji/unicode_support_map');
const spreadString = require('./gl_emoji/spread_string');
installCustomElements(window);
function emojiImageTag(name, src) {
return `<img class="emoji" title=":${name}:" alt=":${name}:" src="${src}" width="20" height="20" align="absmiddle" />`;
}
const glEmojiTagDefaults = {
sprite: false,
forceFallback: false,
};
function glEmojiTag(inputName, options) {
const opts = Object.assign({}, glEmojiTagDefaults, options);
const name = emojiAliases[inputName] || inputName;
const emojiInfo = emojiMap[name];
const fallbackImageSrc = `${gon.relative_url_root || ''}/assets/emoji/${name}-${emojiInfo.digest}.png`;
const fallbackSpriteClass = `emoji-${name}`;
const classList = [];
if (opts.forceFallback && opts.sprite) {
classList.push('emoji-icon');
classList.push(fallbackSpriteClass);
}
const classAttribute = classList.length > 0 ? `class="${classList.join(' ')}"` : '';
const fallbackSpriteAttribute = opts.sprite ? `data-fallback-sprite-class="${fallbackSpriteClass}"` : '';
let contents = emojiInfo.moji;
if (opts.forceFallback && !opts.sprite) {
contents = emojiImageTag(name, fallbackImageSrc);
}
return `
<gl-emoji
${classAttribute}
data-name="${name}"
data-fallback-src="${fallbackImageSrc}"
${fallbackSpriteAttribute}
data-unicode-version="${emojiInfo.unicodeVersion}"
>
${contents}
</gl-emoji>
`;
}
// On Windows, flags render as two-letter country codes, see http://emojipedia.org/flags/
const flagACodePoint = 127462; // parseInt('1F1E6', 16)
const flagZCodePoint = 127487; // parseInt('1F1FF', 16)
function isFlagEmoji(emojiUnicode) {
const cp = emojiUnicode.codePointAt(0);
// Length 4 because flags are made of 2 characters which are surrogate pairs
return emojiUnicode.length === 4 && cp >= flagACodePoint && cp <= flagZCodePoint;
}
// Chrome <57 renders keycaps oddly
// See https://bugs.chromium.org/p/chromium/issues/detail?id=632294
// Same issue on Windows also fixed in Chrome 57, http://i.imgur.com/rQF7woO.png
function isKeycapEmoji(emojiUnicode) {
return emojiUnicode.length === 3 && emojiUnicode[2] === '\u20E3';
}
// Check for a skin tone variation emoji which aren't always supported
const tone1 = 127995;// parseInt('1F3FB', 16)
const tone5 = 127999;// parseInt('1F3FF', 16)
function isSkinToneComboEmoji(emojiUnicode) {
return emojiUnicode.length > 2 && spreadString(emojiUnicode).some((char) => {
const cp = char.codePointAt(0);
return cp >= tone1 && cp <= tone5;
});
}
// macOS supports most skin tone emoji's but
// doesn't support the skin tone versions of horse racing
const horseRacingCodePoint = 127943;// parseInt('1F3C7', 16)
function isHorceRacingSkinToneComboEmoji(emojiUnicode) {
return spreadString(emojiUnicode)[0].codePointAt(0) === horseRacingCodePoint &&
isSkinToneComboEmoji(emojiUnicode);
}
// Check for `family_*`, `kiss_*`, `couple_*`
// For ex. Windows 8.1 Firefox 51.0.1, doesn't support these
const zwj = 8205; // parseInt('200D', 16)
const personStartCodePoint = 128102; // parseInt('1F466', 16)
const personEndCodePoint = 128105; // parseInt('1F469', 16)
function isPersonZwjEmoji(emojiUnicode) {
let hasPersonEmoji = false;
let hasZwj = false;
spreadString(emojiUnicode).forEach((character) => {
const cp = character.codePointAt(0);
if (cp === zwj) {
hasZwj = true;
} else if (cp >= personStartCodePoint && cp <= personEndCodePoint) {
hasPersonEmoji = true;
}
});
return hasPersonEmoji && hasZwj;
}
// Helper so we don't have to run `isFlagEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isFlagResult = isFlagEmoji(emojiUnicode);
return (
(unicodeSupportMap.flag && isFlagResult) ||
!isFlagResult
);
}
// Helper so we don't have to run `isSkinToneComboEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) {
const isSkinToneResult = isSkinToneComboEmoji(emojiUnicode);
return (
(unicodeSupportMap.skinToneModifier && isSkinToneResult) ||
!isSkinToneResult
);
}
// Helper func so we don't have to run `isHorceRacingSkinToneComboEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isHorseRacingSkinToneResult = isHorceRacingSkinToneComboEmoji(emojiUnicode);
return (
(unicodeSupportMap.horseRacing && isHorseRacingSkinToneResult) ||
!isHorseRacingSkinToneResult
);
}
// Helper so we don't have to run `isPersonZwjEmoji` twice
// in `isEmojiUnicodeSupported` logic
function checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode) {
const isPersonZwjResult = isPersonZwjEmoji(emojiUnicode);
return (
(unicodeSupportMap.personZwj && isPersonZwjResult) ||
!isPersonZwjResult
);
}
// Takes in a support map and determines whether
// the given unicode emoji is supported on the platform.
//
// Combines all the edge case tests into a one-stop shop method
function isEmojiUnicodeSupported(unicodeSupportMap = {}, emojiUnicode, unicodeVersion) {
const isOlderThanChrome57 = unicodeSupportMap.meta && unicodeSupportMap.meta.isChrome &&
unicodeSupportMap.meta.chromeVersion < 57;
// For comments about each scenario, see the comments above each individual respective function
return unicodeSupportMap[unicodeVersion] &&
!(isOlderThanChrome57 && isKeycapEmoji(emojiUnicode)) &&
checkFlagEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkSkinToneModifierSupport(unicodeSupportMap, emojiUnicode) &&
checkHorseRacingSkinToneComboEmojiSupport(unicodeSupportMap, emojiUnicode) &&
checkPersonEmojiSupport(unicodeSupportMap, emojiUnicode);
}
const GlEmojiElementProto = Object.create(HTMLElement.prototype);
GlEmojiElementProto.createdCallback = function createdCallback() {
const emojiUnicode = this.textContent.trim();
const {
unicodeVersion,
fallbackSrc,
fallbackSpriteClass,
} = this.dataset;
const isEmojiUnicode = this.childNodes && Array.prototype.every.call(
this.childNodes,
childNode => childNode.nodeType === 3,
);
const hasImageFallback = fallbackSrc && fallbackSrc.length > 0;
const hasCssSpriteFalback = fallbackSpriteClass && fallbackSpriteClass.length > 0;
if (
isEmojiUnicode &&
!isEmojiUnicodeSupported(generatedUnicodeSupportMap, emojiUnicode, unicodeVersion)
) {
// CSS sprite fallback takes precedence over image fallback
if (hasCssSpriteFalback) {
// IE 11 doesn't like adding multiple at once :(
this.classList.add('emoji-icon');
this.classList.add(fallbackSpriteClass);
} else if (hasImageFallback) {
const emojiName = this.dataset.name;
this.innerHTML = emojiImageTag(emojiName, fallbackSrc);
}
}
};
document.registerElement('gl-emoji', {
prototype: GlEmojiElementProto,
});
module.exports = {
emojiImageTag,
glEmojiTag,
isEmojiUnicodeSupported,
isFlagEmoji,
isKeycapEmoji,
isSkinToneComboEmoji,
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
};
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt#Fixing_charCodeAt()_to_handle_non-Basic-Multilingual-Plane_characters_if_their_presence_earlier_in_the_string_is_known
function knownCharCodeAt(givenString, index) {
const str = `${givenString}`;
const end = str.length;
const surrogatePairs = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g;
let idx = index;
while ((surrogatePairs.exec(str)) != null) {
const li = surrogatePairs.lastIndex;
if (li - 2 < idx) {
idx += 1;
} else {
break;
}
}
if (idx >= end || idx < 0) {
return NaN;
}
const code = str.charCodeAt(idx);
let high;
let low;
if (code >= 0xD800 && code <= 0xDBFF) {
high = code;
low = str.charCodeAt(idx + 1);
// Go one further, since one of the "characters" is part of a surrogate pair
return ((high - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000;
}
return code;
}
// See http://stackoverflow.com/a/38901550/796832
// ES5/PhantomJS compatible version of spreading a string
//
// [...'foo'] -> ['f', 'o', 'o']
// [...'🖐🏿'] -> ['🖐', '🏿']
function spreadString(str) {
const arr = [];
let i = 0;
while (!isNaN(knownCharCodeAt(str, i))) {
const codePoint = knownCharCodeAt(str, i);
arr.push(String.fromCodePoint(codePoint));
i += 1;
}
return arr;
}
module.exports = spreadString;
const unicodeSupportTestMap = {
// man, student (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// occupationZwj: '\u{1F468}\u{200D}\u{1F393}',
// woman, biking (emojione does not have any of these yet), http://emojipedia.org/emoji-zwj-sequences/
// sexZwj: '\u{1F6B4}\u{200D}\u{2640}',
// family_mwgb
// Windows 8.1, Firefox 51.0.1 does not support `family_`, `kiss_`, `couple_`
personZwj: '\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}',
// horse_racing_tone5
// Special case that is not supported on macOS 10.12 even though `skinToneModifier` succeeds
horseRacing: '\u{1F3C7}\u{1F3FF}',
// US flag, http://emojipedia.org/flags/
flag: '\u{1F1FA}\u{1F1F8}',
// http://emojipedia.org/modifiers/
skinToneModifier: [
// spy_tone5
'\u{1F575}\u{1F3FF}',
// person_with_ball_tone5
'\u{26F9}\u{1F3FF}',
// angel_tone5
'\u{1F47C}\u{1F3FF}',
],
// rofl, http://emojipedia.org/unicode-9.0/
'9.0': '\u{1F923}',
// metal, http://emojipedia.org/unicode-8.0/
'8.0': '\u{1F918}',
// spy, http://emojipedia.org/unicode-7.0/
'7.0': '\u{1F575}',
// expressionless, http://emojipedia.org/unicode-6.1/
6.1: '\u{1F611}',
// japanese_goblin, http://emojipedia.org/unicode-6.0/
'6.0': '\u{1F47A}',
// sailboat, http://emojipedia.org/unicode-5.2/
5.2: '\u{26F5}',
// mahjong, http://emojipedia.org/unicode-5.1/
5.1: '\u{1F004}',
// gear, http://emojipedia.org/unicode-4.1/
4.1: '\u{2699}',
// zap, http://emojipedia.org/unicode-4.0/
'4.0': '\u{26A1}',
// recycle, http://emojipedia.org/unicode-3.2/
3.2: '\u{267B}',
// information_source, http://emojipedia.org/unicode-3.0/
'3.0': '\u{2139}',
// heart, http://emojipedia.org/unicode-1.1/
1.1: '\u{2764}',
};
function checkPixelInImageDataArray(pixelOffset, imageDataArray) {
// `4 *` because RGBA
const indexOffset = 4 * pixelOffset;
const hasColor = imageDataArray[indexOffset + 0] ||
imageDataArray[indexOffset + 1] ||
imageDataArray[indexOffset + 2];
const isVisible = imageDataArray[indexOffset + 3];
// Check for some sort of color other than black
if (hasColor && isVisible) {
return true;
}
return false;
}
const chromeMatches = navigator.userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
const isChrome = chromeMatches && chromeMatches.length > 0;
const chromeVersion = chromeMatches && chromeMatches[1] && parseInt(chromeMatches[1], 10);
// We use 16px because mobile Safari (iOS 9.3) doesn't properly scale emojis :/
// See 32px, https://i.imgur.com/htY6Zym.png
// See 16px, https://i.imgur.com/FPPsIF8.png
const fontSize = 16;
function testUnicodeSupportMap(testMap) {
const testMapKeys = Object.keys(testMap);
const numTestEntries = testMapKeys
.reduce((list, testKey) => list.concat(testMap[testKey]), []).length;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = (2 * fontSize);
canvas.height = (numTestEntries * fontSize);
ctx.fillStyle = '#000000';
ctx.textBaseline = 'middle';
ctx.font = `${fontSize}px "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"`;
// Write each emoji to the canvas vertically
let writeIndex = 0;
testMapKeys.forEach((testKey) => {
const testEntry = testMap[testKey];
[].concat(testEntry).forEach((emojiUnicode) => {
ctx.fillText(emojiUnicode, 0, (writeIndex * fontSize) + (fontSize / 2));
writeIndex += 1;
});
});
// Read from the canvas
const resultMap = {};
let readIndex = 0;
testMapKeys.forEach((testKey) => {
const testEntry = testMap[testKey];
const isTestSatisfied = [].concat(testEntry).every(() => {
// Sample along the vertical-middle for a couple of characters
const imageData = ctx.getImageData(
0,
(readIndex * fontSize) + (fontSize / 2),
2 * fontSize,
1,
).data;
let isValidEmoji = false;
for (let currentPixel = 0; currentPixel < 64; currentPixel += 1) {
const isLookingAtFirstChar = currentPixel < fontSize;
const isLookingAtSecondChar = currentPixel >= (fontSize + (fontSize / 2));
// Check for the emoji somewhere along the row
if (isLookingAtFirstChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = true;
// Check to see that nothing is rendered next to the first character
// to ensure that the ZWJ sequence rendered as one piece
} else if (isLookingAtSecondChar && checkPixelInImageDataArray(currentPixel, imageData)) {
isValidEmoji = false;
break;
}
}
readIndex += 1;
return isValidEmoji;
});
resultMap[testKey] = isTestSatisfied;
});
resultMap.meta = {
isChrome,
chromeVersion,
};
return resultMap;
}
let unicodeSupportMap;
const userAgentFromCache = window.localStorage.getItem('gl-emoji-user-agent');
try {
unicodeSupportMap = JSON.parse(window.localStorage.getItem('gl-emoji-unicode-support-map'));
} catch (err) {
// swallow
}
if (!unicodeSupportMap || userAgentFromCache !== navigator.userAgent) {
unicodeSupportMap = testUnicodeSupportMap(unicodeSupportTestMap);
window.localStorage.setItem('gl-emoji-user-agent', navigator.userAgent);
window.localStorage.setItem('gl-emoji-unicode-support-map', JSON.stringify(unicodeSupportMap));
}
module.exports = unicodeSupportMap;
...@@ -46,8 +46,8 @@ require('./lib/utils/common_utils'); ...@@ -46,8 +46,8 @@ require('./lib/utils/common_utils');
}, },
}, },
EmojiFilter: { EmojiFilter: {
'img.emoji'(el, text) { 'gl-emoji'(el, text) {
return el.getAttribute('alt'); return `:${el.getAttribute('data-name')}:`;
}, },
}, },
ImageLinkFilter: { ImageLinkFilter: {
......
require('string.prototype.codepointat');
require('string.fromcodepoint');
/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */ /* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, vars-on-top, max-len */
const emojiMap = require('emoji-map');
const emojiAliases = require('emoji-aliases');
const glEmoji = require('./behaviors/gl_emoji');
const glEmojiTag = glEmoji.glEmojiTag;
// Creates the variables for setting up GFM auto-completion // Creates the variables for setting up GFM auto-completion
(function() { (function() {
if (window.gl == null) { if (window.gl == null) {
...@@ -26,7 +32,12 @@ ...@@ -26,7 +32,12 @@
}, },
// Emoji // Emoji
Emoji: { Emoji: {
template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>' templateFunction: function(name) {
return `<li>
${name} ${glEmojiTag(name)}
</li>
`;
}
}, },
// Team Members // Team Members
Members: { Members: {
...@@ -113,7 +124,7 @@ ...@@ -113,7 +124,7 @@
$input.atwho({ $input.atwho({
at: ':', at: ':',
displayTpl: function(value) { displayTpl: function(value) {
return value.path != null ? this.Emoji.template : this.Loading.template; return value && value.name ? this.Emoji.templateFunction(value.name) : this.Loading.template;
}.bind(this), }.bind(this),
insertTpl: ':${name}:', insertTpl: ':${name}:',
skipSpecialCharacterTest: true, skipSpecialCharacterTest: true,
...@@ -355,6 +366,8 @@ ...@@ -355,6 +366,8 @@
this.isLoadingData[at] = true; this.isLoadingData[at] = true;
if (this.cachedData[at]) { if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]); this.loadData($input, at, this.cachedData[at]);
} else if (this.atTypeMap[at] === 'emojis') {
this.loadData($input, at, Object.keys(emojiMap).concat(Object.keys(emojiAliases)));
} else { } else {
$.getJSON(this.dataSources[this.atTypeMap[at]], (data) => { $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
this.loadData($input, at, data); this.loadData($input, at, data);
......
...@@ -3,7 +3,6 @@ ...@@ -3,7 +3,6 @@
/* global Cookies */ /* global Cookies */
/* global Flash */ /* global Flash */
/* global ConfirmDangerModal */ /* global ConfirmDangerModal */
/* global AwardsHandler */
/* global Aside */ /* global Aside */
import jQuery from 'jquery'; import jQuery from 'jquery';
...@@ -19,6 +18,15 @@ require('mousetrap/plugins/pause/mousetrap-pause'); ...@@ -19,6 +18,15 @@ require('mousetrap/plugins/pause/mousetrap-pause');
require('vendor/fuzzaldrin-plus'); require('vendor/fuzzaldrin-plus');
require('es6-promise').polyfill(); require('es6-promise').polyfill();
// extensions
require('./extensions/string');
require('./extensions/array');
require('./extensions/custom_event');
require('./extensions/element');
require('./extensions/jquery');
require('./extensions/object');
require('es6-promise').polyfill();
// expose common libraries as globals (TODO: remove these) // expose common libraries as globals (TODO: remove these)
window.jQuery = jQuery; window.jQuery = jQuery;
window.$ = jQuery; window.$ = jQuery;
...@@ -61,13 +69,6 @@ require('./templates/issuable_template_selectors'); ...@@ -61,13 +69,6 @@ require('./templates/issuable_template_selectors');
require('./commit/file.js'); require('./commit/file.js');
require('./commit/image_file.js'); require('./commit/image_file.js');
// extensions
require('./extensions/array');
require('./extensions/custom_event');
require('./extensions/element');
require('./extensions/jquery');
require('./extensions/object');
// lib/utils // lib/utils
require('./lib/utils/animate'); require('./lib/utils/animate');
require('./lib/utils/bootstrap_linked_tabs'); require('./lib/utils/bootstrap_linked_tabs');
...@@ -99,7 +100,7 @@ require('./ajax_loading_spinner'); ...@@ -99,7 +100,7 @@ require('./ajax_loading_spinner');
require('./api'); require('./api');
require('./aside'); require('./aside');
require('./autosave'); require('./autosave');
require('./awards_handler'); const AwardsHandler = require('./awards_handler');
require('./breakpoints'); require('./breakpoints');
require('./broadcast_message'); require('./broadcast_message');
require('./build'); require('./build');
......
...@@ -44,5 +44,6 @@ ...@@ -44,5 +44,6 @@
@import "framework/images.scss"; @import "framework/images.scss";
@import "framework/broadcast-messages"; @import "framework/broadcast-messages";
@import "framework/emojis.scss"; @import "framework/emojis.scss";
@import "framework/emoji-sprites.scss";
@import "framework/icons.scss"; @import "framework/icons.scss";
@import "framework/snippets.scss"; @import "framework/snippets.scss";
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
.emoji-menu { .emoji-menu {
position: absolute; position: absolute;
top: 0;
margin-top: 3px; margin-top: 3px;
padding: $gl-padding; padding: $gl-padding;
z-index: 9; z-index: 9;
...@@ -20,7 +21,7 @@ ...@@ -20,7 +21,7 @@
opacity: 0; opacity: 0;
transform: scale(.2); transform: scale(.2);
transform-origin: 0 -45px; transform-origin: 0 -45px;
transition: .3s cubic-bezier(.87,-.41,.19,1.44); transition: .3s cubic-bezier(.67,.06,.19,1.44);
transition-property: transform, opacity; transition-property: transform, opacity;
&.is-aligned-right { &.is-aligned-right {
...@@ -47,12 +48,13 @@ ...@@ -47,12 +48,13 @@
} }
.emoji-menu-list { .emoji-menu-list {
list-style: none;
padding-left: 0;
margin-bottom: 0; margin-bottom: 0;
padding-left: 0;
list-style: none;
} }
.emoji-menu-list-item { .emoji-menu-list-item {
float: left;
padding: 3px; padding: 3px;
margin-left: 1px; margin-left: 1px;
margin-right: 1px; margin-right: 1px;
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -248,7 +248,7 @@ $diff-view-modes-border: #c1c1c1; ...@@ -248,7 +248,7 @@ $diff-view-modes-border: #c1c1c1;
* Fonts * Fonts
*/ */
$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace; $monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
/* /*
* Dropdowns * Dropdowns
......
...@@ -188,6 +188,9 @@ ul.notes { ...@@ -188,6 +188,9 @@ ul.notes {
.note-body { .note-body {
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
// Help with emoji cut-off (most noticable in Safari)
// See https://i.imgur.com/0dg87Y9.png
padding-top: 1px;
.note-text { .note-text {
word-wrap: break-word; word-wrap: break-word;
......
class EmojisController < ApplicationController
layout false
def index
end
end
class Projects::AutocompleteSourcesController < Projects::ApplicationController class Projects::AutocompleteSourcesController < Projects::ApplicationController
before_action :load_autocomplete_service, except: [:emojis, :members] before_action :load_autocomplete_service, except: [:members]
def emojis
render json: Gitlab::AwardEmoji.urls
end
def members def members
render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable) render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
......
module EmojiHelper
def emoji_icon(*args)
raw Gitlab::Emoji.gl_emoji_tag(*args)
end
end
...@@ -87,34 +87,6 @@ module IssuesHelper ...@@ -87,34 +87,6 @@ module IssuesHelper
icon('eye-slash') if issue.confidential? icon('eye-slash') if issue.confidential?
end end
def emoji_icon(name, unicode = nil, aliases = [], sprite: true)
unicode ||= Gitlab::Emoji.emoji_filename(name) rescue ""
data = {
aliases: aliases.join(" "),
emoji: name,
unicode_name: unicode
}
if sprite
# Emoji icons for the emoji menu, these use a spritesheet.
content_tag :div, "",
class: "icon emoji-icon emoji-#{unicode}",
title: name,
data: data
else
# Emoji icons displayed separately, used for the awards already given
# to an issue or merge request.
content_tag :img, "",
class: "icon emoji",
title: name,
height: "20px",
width: "20px",
src: url_to_image("#{unicode}.png"),
data: data
end
end
def award_user_list(awards, current_user, limit: 10) def award_user_list(awards, current_user, limit: 10)
names = awards.map do |award| names = awards.map do |award|
award.user == current_user ? 'You' : award.user.name award.user == current_user ? 'You' : award.user.name
......
...@@ -16,4 +16,4 @@ ...@@ -16,4 +16,4 @@
- else - else
.empty-state .empty-state
.text-center .text-center
%h4 There are no abuse reports! #{emoji_icon 'tada'} %h4 There are no abuse reports! #{emoji_icon('tada')}
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
%button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button", %button.btn.award-control.js-emoji-btn.has-tooltip{ type: "button",
class: (award_state_class(awards, current_user)), class: (award_state_class(awards, current_user)),
data: { placement: "bottom", title: award_user_list(awards, current_user) } } data: { placement: "bottom", title: award_user_list(awards, current_user) } }
= emoji_icon(emoji, sprite: false) = emoji_icon(emoji)
%span.award-control-text.js-counter %span.award-control-text.js-counter
= awards.count = awards.count
......
.emoji-menu
= text_field_tag :emoji_search, "", class: "emoji-search search-input form-control", placeholder: "Search emoji"
.emoji-menu-content
- Gitlab::AwardEmoji.emoji_by_category.each do |category, emojis|
%h5.emoji-menu-title
= Gitlab::AwardEmoji::CATEGORIES[category]
%ul.clearfix.emoji-menu-list
- emojis.each do |emoji|
%li.pull-left.text-center.emoji-menu-list-item
%button.emoji-menu-btn.text-center.js-emoji-btn{ type: "button" }
= emoji_icon(emoji["name"], emoji["unicode"], emoji["aliases"])
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
- if project - if project
:javascript :javascript
gl.GfmAutoComplete.dataSources = { gl.GfmAutoComplete.dataSources = {
emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}",
members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}", members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}", issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}",
mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}", mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}",
......
---
title: Use native unicode emojis
merge_request:
author:
...@@ -91,7 +91,6 @@ module Gitlab ...@@ -91,7 +91,6 @@ module Gitlab
# Enable the asset pipeline # Enable the asset pipeline
config.assets.enabled = true config.assets.enabled = true
config.assets.paths << Gemojione.images_path
config.assets.paths << "vendor/assets/fonts" config.assets.paths << "vendor/assets/fonts"
config.assets.precompile << "*.png" config.assets.precompile << "*.png"
config.assets.precompile << "print.css" config.assets.precompile << "print.css"
......
...@@ -27,9 +27,6 @@ Rails.application.routes.draw do ...@@ -27,9 +27,6 @@ Rails.application.routes.draw do
get '/autocomplete/users/:id' => 'autocomplete#user' get '/autocomplete/users/:id' => 'autocomplete#user'
get '/autocomplete/projects' => 'autocomplete#projects' get '/autocomplete/projects' => 'autocomplete#projects'
# Emojis
resources :emojis, only: :index
# Search # Search
get 'search' => 'search#show' get 'search' => 'search#show'
get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete get 'search/autocomplete' => 'search#autocomplete', as: :search_autocomplete
......
...@@ -13,7 +13,6 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -13,7 +13,6 @@ constraints(ProjectUrlConstrainer.new) do
resources :autocomplete_sources, only: [] do resources :autocomplete_sources, only: [] do
collection do collection do
get 'emojis'
get 'members' get 'members'
get 'issues' get 'issues'
get 'merge_requests' get 'merge_requests'
......
...@@ -132,6 +132,7 @@ var config = { ...@@ -132,6 +132,7 @@ var config = {
extensions: ['.js', '.es6', '.js.es6'], extensions: ['.js', '.es6', '.js.es6'],
alias: { alias: {
'~': path.join(ROOT_PATH, 'app/assets/javascripts'), '~': path.join(ROOT_PATH, 'app/assets/javascripts'),
'emoji-map$': path.join(ROOT_PATH, 'fixtures/emojis/digests.json'),
'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'), 'emoji-aliases$': path.join(ROOT_PATH, 'fixtures/emojis/aliases.json'),
'icons': path.join(ROOT_PATH, 'app/views/shared/icons'), 'icons': path.join(ROOT_PATH, 'app/views/shared/icons'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'), 'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
......
...@@ -90,7 +90,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps ...@@ -90,7 +90,7 @@ class Spinach::Features::AwardEmoji < Spinach::FeatureSteps
step 'I see search result for "hand"' do step 'I see search result for "hand"' do
page.within '.emoji-menu-content' do page.within '.emoji-menu-content' do
expect(page).to have_selector '[data-emoji="raised_hand"]' expect(page).to have_selector '[data-name="raised_hand"]'
end end
end end
......
This diff is collapsed.
...@@ -17,8 +17,8 @@ module Banzai ...@@ -17,8 +17,8 @@ module Banzai
next unless content.include?(':') || node.text.match(emoji_unicode_pattern) next unless content.include?(':') || node.text.match(emoji_unicode_pattern)
html = emoji_name_image_filter(content) html = emoji_unicode_element_unicode_filter(content)
html = emoji_unicode_image_filter(html) html = emoji_name_element_unicode_filter(html)
next if html == content next if html == content
...@@ -27,33 +27,30 @@ module Banzai ...@@ -27,33 +27,30 @@ module Banzai
doc doc
end end
# Replace :emoji: with corresponding images. # Replace :emoji: with corresponding gl-emoji unicode.
# #
# text - String text to replace :emoji: in. # text - String text to replace :emoji: in.
# #
# Returns a String with :emoji: replaced with images. # Returns a String with :emoji: replaced with gl-emoji unicode.
def emoji_name_image_filter(text) def emoji_name_element_unicode_filter(text)
text.gsub(emoji_pattern) do |match| text.gsub(emoji_pattern) do |match|
name = $1 name = $1
emoji_image_tag(name, emoji_url(name)) Gitlab::Emoji.gl_emoji_tag(name)
end end
end end
# Replace unicode emoji with corresponding images if they exist. # Replace unicode emoji with corresponding gl-emoji unicode.
# #
# text - String text to replace unicode emoji in. # text - String text to replace unicode emoji in.
# #
# Returns a String with unicode emoji replaced with images. # Returns a String with unicode emoji replaced with gl-emoji unicode.
def emoji_unicode_image_filter(text) def emoji_unicode_element_unicode_filter(text)
text.gsub(emoji_unicode_pattern) do |moji| text.gsub(emoji_unicode_pattern) do |moji|
emoji_image_tag(Gitlab::Emoji.emojis_by_moji[moji]['name'], emoji_unicode_url(moji)) emoji_info = Gitlab::Emoji.emojis_by_moji[moji]
Gitlab::Emoji.gl_emoji_tag(emoji_info['name'])
end end
end end
def emoji_image_tag(emoji_name, emoji_url)
"<img class='emoji' title=':#{emoji_name}:' alt=':#{emoji_name}:' src='#{emoji_url}' height='20' width='20' align='absmiddle' />"
end
# Build a regexp that matches all valid :emoji: names. # Build a regexp that matches all valid :emoji: names.
def self.emoji_pattern def self.emoji_pattern
@emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/ @emoji_pattern ||= /:(#{Gitlab::Emoji.emojis_names.map { |name| Regexp.escape(name) }.join('|')}):/
...@@ -66,52 +63,13 @@ module Banzai ...@@ -66,52 +63,13 @@ module Banzai
private private
def emoji_url(name)
emoji_path = emoji_filename(name)
if context[:asset_host]
# Asset host is specified.
url_to_image(emoji_path)
elsif context[:asset_root]
# Gitlab url is specified
File.join(context[:asset_root], url_to_image(emoji_path))
else
# All other cases
url_to_image(emoji_path)
end
end
def emoji_unicode_url(moji)
emoji_unicode_path = emoji_unicode_filename(moji)
if context[:asset_host]
url_to_image(emoji_unicode_path)
elsif context[:asset_root]
File.join(context[:asset_root], url_to_image(emoji_unicode_path))
else
url_to_image(emoji_unicode_path)
end
end
def url_to_image(image)
ActionController::Base.helpers.url_to_image(image)
end
def emoji_pattern def emoji_pattern
self.class.emoji_pattern self.class.emoji_pattern
end end
def emoji_filename(name)
"#{Gitlab::Emoji.emoji_filename(name)}.png"
end
def emoji_unicode_pattern def emoji_unicode_pattern
self.class.emoji_unicode_pattern self.class.emoji_unicode_pattern
end end
def emoji_unicode_filename(name)
"#{Gitlab::Emoji.emoji_unicode_filename(name)}.png"
end
end end
end end
end end
module Gitlab module Gitlab
class AwardEmoji class AwardEmoji
CATEGORIES = {
objects: "Objects",
travel: "Travel",
symbols: "Symbols",
nature: "Nature",
people: "People",
activity: "Activity",
flags: "Flags",
food: "Food"
}.with_indifferent_access
def self.normalize_emoji_name(name) def self.normalize_emoji_name(name)
aliases[name] || name aliases[name] || name
end end
def self.emoji_by_category
unless @emoji_by_category
@emoji_by_category = Hash.new { |h, key| h[key] = [] }
emojis.each do |emoji_name, data|
data["name"] = emoji_name
# Skip Fitzpatrick(tone) modifiers
next if data["category"] == "modifier"
category = data["category"]
@emoji_by_category[category] << data
end
@emoji_by_category = @emoji_by_category.sort.to_h
end
@emoji_by_category
end
def self.emojis def self.emojis
@emojis ||= Gitlab::Emoji.emojis
begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'index.json' )
JSON.parse(File.read(json_path))
end
end end
def self.aliases def self.aliases
@aliases ||= Gitlab::Emoji.emojis_aliases
begin
json_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')
JSON.parse(File.read(json_path))
end
end
# Returns an Array of Emoji names and their asset URLs.
def self.urls
@urls ||= begin
path = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
# Construct the full asset path ourselves because
# ActionView::Helpers::AssetUrlHelper.asset_url is slow for hundreds
# of entries since it has to do a lot of extra work (e.g. regexps).
prefix = Gitlab::Application.config.assets.prefix
digest = Gitlab::Application.config.assets.digest
base =
if defined?(Gitlab::Application.config.relative_url_root) && Gitlab::Application.config.relative_url_root
Gitlab::Application.config.relative_url_root
else
''
end
JSON.parse(File.read(path)).map do |hash|
fname =
if digest
"#{hash['unicode']}-#{hash['digest']}"
else
hash['unicode']
end
{ name: hash['name'], path: File.join(base, prefix, "#{fname}.png") }
end
end
end end
end end
end end
module Gitlab module Gitlab
module Emoji module Emoji
extend self extend self
@emoji_unicode_version = JSON.parse(File.read(File.absolute_path(File.dirname(__FILE__) + '/../../node_modules/emoji-unicode-version/emoji-unicode-version-map.json')))
@emoji_aliases = JSON.parse(File.read(File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json')))
def emojis def emojis
Gemojione.index.instance_variable_get(:@emoji_by_name) Gemojione.index.instance_variable_get(:@emoji_by_name)
end end
...@@ -18,6 +20,10 @@ module Gitlab ...@@ -18,6 +20,10 @@ module Gitlab
emojis.keys emojis.keys
end end
def emojis_aliases
@emoji_aliases
end
def emoji_filename(name) def emoji_filename(name)
emojis[name]["unicode"] emojis[name]["unicode"]
end end
...@@ -25,5 +31,22 @@ module Gitlab ...@@ -25,5 +31,22 @@ module Gitlab
def emoji_unicode_filename(moji) def emoji_unicode_filename(moji)
emojis_by_moji[moji]["unicode"] emojis_by_moji[moji]["unicode"]
end end
def emoji_unicode_version(name)
@emoji_unicode_version[name]
end
def emoji_image_tag(name, src)
"<img class='emoji' title=':#{name}:' alt=':#{name}:' src='#{src}' height='20' width='20' align='absmiddle' />"
end
# CSS sprite fallback takes precedence over image fallback
def gl_emoji_tag(name, sprite: false, force_fallback: false)
emoji_name = emojis_aliases[name] || name
emoji_info = emojis[emoji_name]
emoji_fallback_image_source = ActionController::Base.helpers.asset_url("emoji/#{emoji_info['name']}.png")
emoji_fallback_sprite_class = "emoji-#{emoji_name}"
"<gl-emoji #{force_fallback && sprite ? "class='emoji-icon #{emoji_fallback_sprite_class}'" : ""} data-name='#{emoji_name}' data-fallback-src='#{emoji_fallback_image_source}' #{sprite ? "data-fallback-sprite-class='#{emoji_fallback_sprite_class}'" : ""} data-unicode-version='#{emoji_unicode_version(emoji_name)}'>#{force_fallback && sprite === false ? emoji_image_tag(emoji_name, emoji_fallback_image_source) : emoji_info['moji']}</gl-emoji>"
end
end end
end end
...@@ -7,7 +7,6 @@ module Gitlab ...@@ -7,7 +7,6 @@ module Gitlab
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.shortcuts_path = help_page_path('shortcuts') gon.shortcuts_path = help_page_path('shortcuts')
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
gon.award_menu_url = emojis_path
gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css') gon.katex_css_url = ActionController::Base.helpers.asset_path('katex.css')
gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js') gon.katex_js_url = ActionController::Base.helpers.asset_path('katex.js')
......
...@@ -5,29 +5,29 @@ namespace :gemojione do ...@@ -5,29 +5,29 @@ namespace :gemojione do
require 'json' require 'json'
dir = Gemojione.images_path dir = Gemojione.images_path
digests = [] resultant_emoji_map = {}
aliases = Hash.new { |hash, key| hash[key] = [] }
aliases_path = File.join(Rails.root, 'fixtures', 'emojis', 'aliases.json') Gitlab::Emoji.emojis.map do |name, emoji_hash|
# Ignore aliases
JSON.parse(File.read(aliases_path)).each do |alias_name, real_name| unless Gitlab::Emoji.emojis_aliases.key?(name)
aliases[real_name] << alias_name fpath = File.join(dir, "#{emoji_hash['unicode']}.png")
end hash_digest = Digest::SHA256.file(fpath).hexdigest
Gitlab::AwardEmoji.emojis.map do |name, emoji_hash| entry = {
fpath = File.join(dir, "#{emoji_hash['unicode']}.png") category: emoji_hash['category'],
digest = Digest::SHA256.file(fpath).hexdigest moji: emoji_hash['moji'],
unicodeVersion: Gitlab::Emoji.emoji_unicode_version(name),
digests << { name: name, unicode: emoji_hash['unicode'], digest: digest } digest: hash_digest,
}
aliases[name].each do |alias_name| resultant_emoji_map[name] = entry
digests << { name: alias_name, unicode: emoji_hash['unicode'], digest: digest }
end end
end end
out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json') out = File.join(Rails.root, 'fixtures', 'emojis', 'digests.json')
File.open(out, 'w') do |handle| File.open(out, 'w') do |handle|
handle.write(JSON.pretty_generate(digests)) handle.write(JSON.pretty_generate(resultant_emoji_map))
end end
end end
...@@ -55,21 +55,42 @@ namespace :gemojione do ...@@ -55,21 +55,42 @@ namespace :gemojione do
SPRITESHEET_WIDTH = 860 SPRITESHEET_WIDTH = 860
SPRITESHEET_HEIGHT = 840 SPRITESHEET_HEIGHT = 840
# Setup a map to rename image files
emoji_uncicode_string_to_name_map = {}
Gitlab::Emoji.emojis.map do |name, emoji_hash|
# Ignore aliases
unless Gitlab::Emoji.emojis_aliases.key?(name)
emoji_uncicode_string_to_name_map[emoji_hash['unicode']] = name
end
end
# Copy the Gemojione assets to the temporary folder for renaming
emoji_dir = "app/assets/images/emoji"
FileUtils.rm_rf(emoji_dir)
FileUtils.mkdir_p(emoji_dir, mode: 0700)
FileUtils.cp_r(File.join(Gemojione.images_path, '.'), emoji_dir)
Dir.chdir(emoji_dir) do
Dir["**/*.png"].each do |png|
image_path = File.join(Dir.pwd, png)
rename_to_named_emoji_image!(emoji_uncicode_string_to_name_map, image_path)
end
end
Dir.mktmpdir do |tmpdir| Dir.mktmpdir do |tmpdir|
# Copy the Gemojione assets to the temporary folder for resizing FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir)
FileUtils.cp_r(Gemojione.images_path, tmpdir)
Dir.chdir(tmpdir) do Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png| Dir["**/*.png"].each do |png|
resize!(File.join(tmpdir, png), SIZE) tmp_image_path = File.join(tmpdir, png)
resize!(tmp_image_path, SIZE)
end end
end end
style_path = Rails.root.join(*%w(app assets stylesheets pages emojis.scss)) style_path = Rails.root.join(*%w(app assets stylesheets framework emoji-sprites.scss))
# Combine the resized assets into a packed sprite and re-generate the SCSS # Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.cssurl = "image-url('$IMAGE')" SpriteFactory.cssurl = "image-url('$IMAGE')"
SpriteFactory.run!(File.join(tmpdir, 'png'), { SpriteFactory.run!(tmpdir, {
output_style: style_path, output_style: style_path,
output_image: "app/assets/images/emoji.png", output_image: "app/assets/images/emoji.png",
selector: '.emoji-', selector: '.emoji-',
...@@ -83,7 +104,7 @@ namespace :gemojione do ...@@ -83,7 +104,7 @@ namespace :gemojione do
# let's simplify it # let's simplify it
system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path})) system(%Q(sed -i '' "s/width: #{SIZE}px; height: #{SIZE}px; background: image-url('emoji.png')/background-position:/" #{style_path}))
system(%Q(sed -i '' "s/ no-repeat//" #{style_path})) system(%Q(sed -i '' "s/ no-repeat//" #{style_path}))
system(%Q(sed -i '' "s/ 0px/ 0/" #{style_path})) system(%Q(sed -i '' "s/ 0px/ 0/g" #{style_path}))
# Append a generic rule that applies to all Emojis # Append a generic rule that applies to all Emojis
File.open(style_path, 'a') do |f| File.open(style_path, 'a') do |f|
...@@ -92,6 +113,8 @@ namespace :gemojione do ...@@ -92,6 +113,8 @@ namespace :gemojione do
.emoji-icon { .emoji-icon {
background-image: image-url('emoji.png'); background-image: image-url('emoji.png');
background-repeat: no-repeat; background-repeat: no-repeat;
color: transparent;
text-indent: -99em;
height: #{SIZE}px; height: #{SIZE}px;
width: #{SIZE}px; width: #{SIZE}px;
...@@ -112,16 +135,17 @@ namespace :gemojione do ...@@ -112,16 +135,17 @@ namespace :gemojione do
# Now do it again but for Retina # Now do it again but for Retina
Dir.mktmpdir do |tmpdir| Dir.mktmpdir do |tmpdir|
# Copy the Gemojione assets to the temporary folder for resizing # Copy the Gemojione assets to the temporary folder for resizing
FileUtils.cp_r(Gemojione.images_path, tmpdir) FileUtils.cp_r(File.join(emoji_dir, '.'), tmpdir)
Dir.chdir(tmpdir) do Dir.chdir(tmpdir) do
Dir["**/*.png"].each do |png| Dir["**/*.png"].each do |png|
resize!(File.join(tmpdir, png), RETINA) tmp_image_path = File.join(tmpdir, png)
resize!(tmp_image_path, RETINA)
end end
end end
# Combine the resized assets into a packed sprite and re-generate the SCSS # Combine the resized assets into a packed sprite and re-generate the SCSS
SpriteFactory.run!(File.join(tmpdir), { SpriteFactory.run!(tmpdir, {
output_image: "app/assets/images/emoji@2x.png", output_image: "app/assets/images/emoji@2x.png",
style: false, style: false,
nocomments: true, nocomments: true,
...@@ -155,4 +179,20 @@ namespace :gemojione do ...@@ -155,4 +179,20 @@ namespace :gemojione do
image.write(image_path) { self.quality = 100 } image.write(image_path) { self.quality = 100 }
image.destroy! image.destroy!
end end
EMOJI_IMAGE_PATH_RE = /(.*?)(([0-9a-f]-?)+)\.png$/i
def rename_to_named_emoji_image!(emoji_uncicode_string_to_name_map, image_path)
# Rename file from unicode to emoji name
matches = EMOJI_IMAGE_PATH_RE.match(image_path)
preceding_path = matches[1]
unicode_string = matches[2]
name = emoji_uncicode_string_to_name_map[unicode_string]
if name
new_png_path = File.join(preceding_path, "#{name}.png")
FileUtils.mv(image_path, new_png_path)
new_png_path
else
puts "Warning: emoji_uncicode_string_to_name_map missing entry for #{unicode_string}. Full path: #{image_path}"
end
end
end end
...@@ -57,7 +57,7 @@ describe "User Feed", feature: true do ...@@ -57,7 +57,7 @@ describe "User Feed", feature: true do
end end
it 'has XHTML summaries in notes' do it 'has XHTML summaries in notes' do
expect(body).to match /Bug confirmed <img[^>]*\/>/ expect(body).to match /Bug confirmed <gl-emoji[^>]*>/
end end
it 'has XHTML summaries in merge request descriptions' do it 'has XHTML summaries in merge request descriptions' do
......
...@@ -252,7 +252,7 @@ describe 'Copy as GFM', feature: true, js: true do ...@@ -252,7 +252,7 @@ describe 'Copy as GFM', feature: true, js: true do
<<-GFM.strip_heredoc <<-GFM.strip_heredoc
<a name="named-anchor"></a> <a name="named-anchor"></a>
<sub>sub</sub> <sub>sub</sub>
<dl> <dl>
......
...@@ -105,7 +105,7 @@ feature 'Group', feature: true do ...@@ -105,7 +105,7 @@ feature 'Group', feature: true do
visit path visit path
expect(page).to have_css('.group-home-desc > p > img') expect(page).to have_css('.group-home-desc > p > gl-emoji')
end end
it 'sanitizes unwanted tags' do it 'sanitizes unwanted tags' do
......
...@@ -25,14 +25,14 @@ describe 'Awards Emoji', feature: true do ...@@ -25,14 +25,14 @@ describe 'Awards Emoji', feature: true do
end end
it 'increments the thumbsdown emoji', js: true do it 'increments the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click find('[data-name="thumbsdown"]').click
wait_for_ajax wait_for_ajax
expect(thumbsdown_emoji).to have_text("1") expect(thumbsdown_emoji).to have_text("1")
end end
context 'click the thumbsup emoji' do context 'click the thumbsup emoji' do
it 'increments the thumbsup emoji', js: true do it 'increments the thumbsup emoji', js: true do
find('[data-emoji="thumbsup"]').click find('[data-name="thumbsup"]').click
wait_for_ajax wait_for_ajax
expect(thumbsup_emoji).to have_text("1") expect(thumbsup_emoji).to have_text("1")
end end
...@@ -44,7 +44,7 @@ describe 'Awards Emoji', feature: true do ...@@ -44,7 +44,7 @@ describe 'Awards Emoji', feature: true do
context 'click the thumbsdown emoji' do context 'click the thumbsdown emoji' do
it 'increments the thumbsdown emoji', js: true do it 'increments the thumbsdown emoji', js: true do
find('[data-emoji="thumbsdown"]').click find('[data-name="thumbsdown"]').click
wait_for_ajax wait_for_ajax
expect(thumbsdown_emoji).to have_text("1") expect(thumbsdown_emoji).to have_text("1")
end end
...@@ -123,9 +123,9 @@ describe 'Awards Emoji', feature: true do ...@@ -123,9 +123,9 @@ describe 'Awards Emoji', feature: true do
end end
unless status unless status
first('[data-emoji="smiley"]').click first('[data-name="smiley"]').click
else else
find('[data-emoji="smiley"]').click find('[data-name="smiley"]').click
end end
wait_for_ajax wait_for_ajax
......
...@@ -18,7 +18,7 @@ feature 'Project', feature: true do ...@@ -18,7 +18,7 @@ feature 'Project', feature: true do
it 'passes through html-pipeline' do it 'passes through html-pipeline' do
project.update_attribute(:description, 'This project is the :poop:') project.update_attribute(:description, 'This project is the :poop:')
visit path visit path
expect(page).to have_css('.project-home-desc > p > img') expect(page).to have_css('.project-home-desc > p > gl-emoji')
end end
it 'sanitizes unwanted tags' do it 'sanitizes unwanted tags' do
......
...@@ -113,7 +113,7 @@ describe GitlabMarkdownHelper do ...@@ -113,7 +113,7 @@ describe GitlabMarkdownHelper do
it 'replaces commit message with emoji to link' do it 'replaces commit message with emoji to link' do
actual = link_to_gfm(':book:Book', '/foo') actual = link_to_gfm(':book:Book', '/foo')
expect(actual). expect(actual).
to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://#{Gitlab.config.gitlab.host}/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>) to eq '<gl-emoji data-name="book" data-fallback-src="/assets/emoji/book.png" data-unicode-version="6.0">📖</gl-emoji><a href="/foo">Book</a>'
end end
end end
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -14,12 +14,12 @@ describe Banzai::Filter::EmojiFilter, lib: true do ...@@ -14,12 +14,12 @@ describe Banzai::Filter::EmojiFilter, lib: true do
it 'replaces supported name emoji' do it 'replaces supported name emoji' do
doc = filter('<p>:heart:</p>') doc = filter('<p>:heart:</p>')
expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png' expect(doc.css('gl-emoji').first.text).to eq '❤'
end end
it 'replaces supported unicode emoji' do it 'replaces supported unicode emoji' do
doc = filter('<p>❤️</p>') doc = filter('<p>❤️</p>')
expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png' expect(doc.css('gl-emoji').first.text).to eq '❤'
end end
it 'ignores unsupported emoji' do it 'ignores unsupported emoji' do
...@@ -30,152 +30,97 @@ describe Banzai::Filter::EmojiFilter, lib: true do ...@@ -30,152 +30,97 @@ describe Banzai::Filter::EmojiFilter, lib: true do
it 'correctly encodes the URL' do it 'correctly encodes the URL' do
doc = filter('<p>:+1:</p>') doc = filter('<p>:+1:</p>')
expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png' expect(doc.css('gl-emoji').first.text).to eq '👍'
end end
it 'correctly encodes unicode to the URL' do it 'correctly encodes unicode to the URL' do
doc = filter('<p>👍</p>') doc = filter('<p>👍</p>')
expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png' expect(doc.css('gl-emoji').first.text).to eq '👍'
end end
it 'matches at the start of a string' do it 'matches at the start of a string' do
doc = filter(':+1:') doc = filter(':+1:')
expect(doc.css('img').size).to eq 1 expect(doc.css('gl-emoji').size).to eq 1
end end
it 'unicode matches at the start of a string' do it 'unicode matches at the start of a string' do
doc = filter("'👍'") doc = filter("'👍'")
expect(doc.css('img').size).to eq 1 expect(doc.css('gl-emoji').size).to eq 1
end end
it 'matches at the end of a string' do it 'matches at the end of a string' do
doc = filter('This gets a :-1:') doc = filter('This gets a :-1:')
expect(doc.css('img').size).to eq 1 expect(doc.css('gl-emoji').size).to eq 1
end end
it 'unicode matches at the end of a string' do it 'unicode matches at the end of a string' do
doc = filter('This gets a 👍') doc = filter('This gets a 👍')
expect(doc.css('img').size).to eq 1 expect(doc.css('gl-emoji').size).to eq 1
end end
it 'matches with adjacent text' do it 'matches with adjacent text' do
doc = filter('+1 (:+1:)') doc = filter('+1 (:+1:)')
expect(doc.css('img').size).to eq 1 expect(doc.css('gl-emoji').size).to eq 1
end end
it 'unicode matches with adjacent text' do it 'unicode matches with adjacent text' do
doc = filter('+1 (👍)') doc = filter('+1 (👍)')
expect(doc.css('img').size).to eq 1 expect(doc.css('gl-emoji').size).to eq 1
end end
it 'matches multiple emoji in a row' do it 'matches multiple emoji in a row' do
doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:') doc = filter(':see_no_evil::hear_no_evil::speak_no_evil:')
expect(doc.css('img').size).to eq 3 expect(doc.css('gl-emoji').size).to eq 3
end end
it 'unicode matches multiple emoji in a row' do it 'unicode matches multiple emoji in a row' do
doc = filter("'🙈🙉🙊'") doc = filter("'🙈🙉🙊'")
expect(doc.css('img').size).to eq 3 expect(doc.css('gl-emoji').size).to eq 3
end end
it 'mixed matches multiple emoji in a row' do it 'mixed matches multiple emoji in a row' do
doc = filter("'🙈:see_no_evil:🙉:hear_no_evil:🙊:speak_no_evil:'") doc = filter("'🙈:see_no_evil:🙉:hear_no_evil:🙊:speak_no_evil:'")
expect(doc.css('img').size).to eq 6 expect(doc.css('gl-emoji').size).to eq 6
end end
it 'has a title attribute' do it 'has a data-name attribute' do
doc = filter(':-1:') doc = filter(':-1:')
expect(doc.css('img').first.attr('title')).to eq ':-1:' expect(doc.css('gl-emoji').first.attr('data-name')).to eq 'thumbsdown'
end end
it 'unicode has a title attribute' do it 'has a data-fallback-src attribute' do
doc = filter("'👎'")
expect(doc.css('img').first.attr('title')).to eq ':thumbsdown:'
end
it 'has an alt attribute' do
doc = filter(':-1:') doc = filter(':-1:')
expect(doc.css('img').first.attr('alt')).to eq ':-1:' expect(doc.css('gl-emoji').first.attr('data-fallback-src')).to end_with '.png'
end
it 'unicode has an alt attribute' do
doc = filter("'👎'")
expect(doc.css('img').first.attr('alt')).to eq ':thumbsdown:'
end
it 'has an align attribute' do
doc = filter(':8ball:')
expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
end
it 'unicode has an align attribute' do
doc = filter("'🎱'")
expect(doc.css('img').first.attr('align')).to eq 'absmiddle'
end
it 'has an emoji class' do
doc = filter(':cat:')
expect(doc.css('img').first.attr('class')).to eq 'emoji'
end
it 'unicode has an emoji class' do
doc = filter("'🐱'")
expect(doc.css('img').first.attr('class')).to eq 'emoji'
end end
it 'has height and width attributes' do it 'has a data-unicode-version attribute' do
doc = filter(':dog:') doc = filter(':-1:')
img = doc.css('img').first expect(doc.css('gl-emoji').first.attr('data-unicode-version')).to eq '6.0'
expect(img.attr('width')).to eq '20'
expect(img.attr('height')).to eq '20'
end
it 'unicode has height and width attributes' do
doc = filter("'🐶'")
img = doc.css('img').first
expect(img.attr('width')).to eq '20'
expect(img.attr('height')).to eq '20'
end end
it 'keeps whitespace intact' do it 'keeps whitespace intact' do
doc = filter('This deserves a :+1:, big time.') doc = filter('This deserves a :+1:, big time.')
expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/) expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
end end
it 'unicode keeps whitespace intact' do it 'unicode keeps whitespace intact' do
doc = filter('This deserves a 🎱, big time.') doc = filter('This deserves a 🎱, big time.')
expect(doc.to_html).to match(/^This deserves a <img.+>, big time\.\z/) expect(doc.to_html).to match(/^This deserves a <gl-emoji.+>, big time\.\z/)
end
it 'uses a custom asset_root context' do
root = Gitlab.config.gitlab.url + 'gitlab/root'
doc = filter(':smile:', asset_root: root)
expect(doc.css('img').first.attr('src')).to start_with(root)
end end
it 'uses a custom asset_host context' do it 'uses a custom asset_host context' do
ActionController::Base.asset_host = 'https://cdn.example.com' ActionController::Base.asset_host = 'https://cdn.example.com'
doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?') doc = filter(':frowning:', asset_host: 'https://this-is-ignored-i-guess?')
expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com') expect(doc.css('gl-emoji').first.attr('data-fallback-src')).to start_with('https://cdn.example.com')
end
it 'uses a custom asset_root context' do
root = Gitlab.config.gitlab.url + 'gitlab/root'
doc = filter("'🎱'", asset_root: root)
expect(doc.css('img').first.attr('src')).to start_with(root)
end end
it 'uses a custom asset_host context' do it 'uses a custom asset_host context' do
ActionController::Base.asset_host = 'https://cdn.example.com' ActionController::Base.asset_host = 'https://cdn.example.com'
doc = filter("'🎱'", asset_host: 'https://this-is-ignored-i-guess?') doc = filter("'🎱'", asset_host: 'https://this-is-ignored-i-guess?')
expect(doc.css('img').first.attr('src')).to start_with('https://cdn.example.com') expect(doc.css('gl-emoji').first.attr('data-fallback-src')).to start_with('https://cdn.example.com')
end end
end end
require 'spec_helper' require 'spec_helper'
describe Gitlab::AwardEmoji do describe Gitlab::AwardEmoji do
describe '.urls' do
after do
Gitlab::AwardEmoji.instance_variable_set(:@urls, nil)
end
subject { Gitlab::AwardEmoji.urls }
it { is_expected.to be_an_instance_of(Array) }
it { is_expected.not_to be_empty }
context 'every Hash in the Array' do
it 'has the correct keys and values' do
subject.each do |hash|
expect(hash[:name]).to be_an_instance_of(String)
expect(hash[:path]).to be_an_instance_of(String)
end
end
end
context 'handles relative root' do
it 'includes the full path' do
allow(Gitlab::Application.config).to receive(:relative_url_root).and_return('/gitlab')
subject.each do |hash|
expect(hash[:name]).to be_an_instance_of(String)
expect(hash[:path]).to start_with('/gitlab')
end
end
end
end
describe '.emoji_by_category' do
it "only contains known categories" do
undefined_categories = Gitlab::AwardEmoji.emoji_by_category.keys - Gitlab::AwardEmoji::CATEGORIES.keys
expect(undefined_categories).to be_empty
end
end
end end
...@@ -120,7 +120,6 @@ describe 'project routing' do ...@@ -120,7 +120,6 @@ describe 'project routing' do
end end
end end
# emojis_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/emojis(.:format) projects/autocomplete_sources#emojis
# members_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/members(.:format) projects/autocomplete_sources#members # members_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/members(.:format) projects/autocomplete_sources#members
# issues_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/issues(.:format) projects/autocomplete_sources#issues # issues_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/issues(.:format) projects/autocomplete_sources#issues
# merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests # merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests
...@@ -128,7 +127,7 @@ describe 'project routing' do ...@@ -128,7 +127,7 @@ describe 'project routing' do
# milestones_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/milestones(.:format) projects/autocomplete_sources#milestones # milestones_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/milestones(.:format) projects/autocomplete_sources#milestones
# commands_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/commands(.:format) projects/autocomplete_sources#commands # commands_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/commands(.:format) projects/autocomplete_sources#commands
describe Projects::AutocompleteSourcesController, 'routing' do describe Projects::AutocompleteSourcesController, 'routing' do
[:emojis, :members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action| [:members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action|
it "to ##{action}" do it "to ##{action}" do
expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq') expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq')
end end
......
...@@ -26,10 +26,10 @@ module MarkdownMatchers ...@@ -26,10 +26,10 @@ module MarkdownMatchers
set_default_markdown_messages set_default_markdown_messages
match do |actual| match do |actual|
expect(actual).to have_selector('img.emoji', count: 10) expect(actual).to have_selector('gl-emoji', count: 10)
image = actual.at_css('img.emoji') emoji_element = actual.at_css('gl-emoji')
expect(image['src'].to_s).to start_with(Gitlab.config.gitlab.url + '/assets') expect(emoji_element['data-fallback-src'].to_s).to start_with('/assets')
end end
end end
......
...@@ -1395,6 +1395,10 @@ doctrine@1.5.0, doctrine@^1.2.2: ...@@ -1395,6 +1395,10 @@ doctrine@1.5.0, doctrine@^1.2.2:
esutils "^2.0.2" esutils "^2.0.2"
isarray "^1.0.0" isarray "^1.0.0"
document-register-element@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/document-register-element/-/document-register-element-1.3.0.tgz#fb3babb523c74662be47be19c6bc33e71990d940"
dom-serialize@^2.2.0: dom-serialize@^2.2.0:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b" resolved "https://registry.yarnpkg.com/dom-serialize/-/dom-serialize-2.2.1.tgz#562ae8999f44be5ea3076f5419dcd59eb43ac95b"
...@@ -1439,6 +1443,10 @@ elliptic@^6.0.0: ...@@ -1439,6 +1443,10 @@ elliptic@^6.0.0:
hash.js "^1.0.0" hash.js "^1.0.0"
inherits "^2.0.1" inherits "^2.0.1"
emoji-unicode-version@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/emoji-unicode-version/-/emoji-unicode-version-0.2.1.tgz#0ebf3666b5414097971d34994e299fce75cdbafc"
emojis-list@^2.0.0: emojis-list@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389"
...@@ -4115,6 +4123,14 @@ string-width@^2.0.0: ...@@ -4115,6 +4123,14 @@ string-width@^2.0.0:
is-fullwidth-code-point "^2.0.0" is-fullwidth-code-point "^2.0.0"
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
string.fromcodepoint@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/string.fromcodepoint/-/string.fromcodepoint-0.2.1.tgz#8d978333c0bc92538f50f383e4888f3e5619d653"
string.prototype.codepointat@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/string.prototype.codepointat/-/string.prototype.codepointat-0.2.0.tgz#6b26e9bd3afcaa7be3b4269b526de1b82000ac78"
string_decoder@^0.10.25, string_decoder@~0.10.x: string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31" version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
......
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