emoji: Resolve emoji sprite sheets and stylesheets through Webpack.

This gives them cache-compatible URLs, and also avoids some extra
copies of the sprite sheet images.

Comments on the Octopus emoji added by tabbott.

Signed-off-by: Anders Kaseorg <anders@zulipchat.com>
This commit is contained in:
Anders Kaseorg 2020-02-20 15:20:39 -08:00 committed by Tim Abbott
parent 197084ab93
commit 1cdab5ae61
17 changed files with 107 additions and 85 deletions

View File

@ -1,3 +1,5 @@
const rewiremock = require("rewiremock/node");
/*
This test suite is designed to find errors
in our initialization sequence. It doesn't
@ -122,7 +124,15 @@ zrequire('top_left_corner');
zrequire('starred_messages');
zrequire('user_status');
zrequire('user_status_ui');
const ui_init = zrequire('ui_init');
const ui_init = rewiremock.proxy(
() => zrequire("ui_init"),
{
"../../static/js/emojisets": {
initialize: () => {},
},
}
);
set_global('$', global.make_zjquery());

1
static/.gitignore vendored
View File

@ -7,6 +7,7 @@
/generated/bots/
# From emoji
/generated/emoji
/generated/emoji-styles
# From passing pygments data to the frontend
/generated/pygments_data.json
# From `tools/update-authors-json`

View File

@ -129,22 +129,6 @@ exports.initialize = function initialize() {
}
exports.update_emojis(page_params.realm_emoji);
let emojiset = page_params.emojiset;
if (page_params.emojiset === 'text') {
// If the current emojiset is `text`, then we fallback to the
// `google` emojiset on the backend (see zerver/views/home.py)
// for displaying emojis in emoji picker and composebox
// typeahead. This logic ensures that we do sprite sheet
// prefetching for that case.
emojiset = 'google-blob';
}
// Load the sprite image and octopus image in the background, so
// that the browser will cache it for later use.
const sprite = new Image();
sprite.src = '/static/generated/emoji/sheet-' + emojiset + '-64.png';
const octopus_image = new Image();
octopus_image.src = '/static/generated/emoji/images-' + emojiset + '-64/1f419.png';
};
exports.build_emoji_data = function (realm_emojis) {

48
static/js/emojisets.js Normal file
View File

@ -0,0 +1,48 @@
import google_blob_css from "!style-loader?injectType=lazyStyleTag!css-loader!../generated/emoji-styles/google-blob-sprite.css";
import google_blob_sheet from "emoji-datasource-google-blob/img/google/sheets-256/64.png";
import google_css from "!style-loader?injectType=lazyStyleTag!css-loader!../generated/emoji-styles/google-sprite.css";
import google_sheet from "emoji-datasource-google/img/google/sheets-256/64.png";
import twitter_css from "!style-loader?injectType=lazyStyleTag!css-loader!../generated/emoji-styles/twitter-sprite.css";
import twitter_sheet from "emoji-datasource-twitter/img/twitter/sheets-256/64.png";
const emojisets = new Map([
["google", { css: google_css, sheet: google_sheet }],
["google-blob", { css: google_blob_css, sheet: google_blob_sheet }],
["twitter", { css: twitter_css, sheet: twitter_sheet }],
]);
// For `text` emojiset we fallback to `google-blob` emojiset
// for displaying emojis in emoji picker and typeahead.
emojisets.set("text", emojisets.get("google-blob"));
let current_emojiset;
export async function select(name) {
const new_emojiset = emojisets.get(name);
if (new_emojiset === current_emojiset) {
return;
}
await new Promise((resolve, reject) => {
const sheet = new Image();
sheet.onload = resolve;
sheet.onerror = reject;
sheet.src = new_emojiset.sheet;
});
if (current_emojiset) {
current_emojiset.css.unuse();
}
new_emojiset.css.use();
current_emojiset = new_emojiset;
}
export function initialize() {
select(page_params.emojiset);
// Load the octopus image in the background, so that the browser
// will cache it for later use. Note that we hardcode the octopus
// emoji to the old Google one because it's better.
//
// TODO: We should probably just make this work just like the Zulip emoji.
const octopus_image = new Image();
octopus_image.src = '/static/generated/emoji/images-google-64/1f419.png';
}

View File

@ -1,3 +1,4 @@
const emojisets = require("./emojisets.js");
const settings_config = require("./settings_config");
const meta = {
@ -121,39 +122,24 @@ exports.set_up = function () {
});
};
exports.report_emojiset_change = function () {
exports.report_emojiset_change = async function () {
// TODO: Clean up how this works so we can use
// change_display_setting. The challenge is that we don't want to
// report success before the server_events request returns that
// causes the actual sprite sheet to change. The current
// implementation is wrong, though, in that it displays the UI
// update in all active browser windows.
function emoji_success() {
if ($("#emoji-settings-status").length) {
loading.destroy_indicator($("#emojiset_spinner"));
$("#emojiset_select").val(page_params.emojiset);
ui_report.success(i18n.t("Emojiset changed successfully!"),
$('#emoji-settings-status').expectOne());
const spinner = $("#emoji-settings-status").expectOne();
settings_ui.display_checkmark(spinner);
}
await emojisets.select(page_params.emojiset);
if ($("#emoji-settings-status").length) {
loading.destroy_indicator($("#emojiset_spinner"));
$("#emojiset_select").val(page_params.emojiset);
ui_report.success(i18n.t("Emojiset changed successfully!"),
$('#emoji-settings-status').expectOne());
const spinner = $("#emoji-settings-status").expectOne();
settings_ui.display_checkmark(spinner);
}
let emojiset = page_params.emojiset;
if (page_params.emojiset === 'text') {
// For `text` emojiset we fallback to `google-blob` emojiset
// for displaying emojis in emoji picker and typeahead.
emojiset = 'google-blob';
}
const sprite = new Image();
sprite.onload = function () {
const sprite_css_href = "/static/generated/emoji/" + emojiset + "-sprite.css";
$("#emoji-spritesheet").attr('href', sprite_css_href);
emoji_success();
};
sprite.src = "/static/generated/emoji/sheet-" + emojiset + "-64.png";
};
exports.update_page = function () {

View File

@ -1,3 +1,4 @@
const emojisets = require("./emojisets");
const markdown_config = require('./markdown_config');
// This is where most of our initialization takes place.
@ -295,6 +296,7 @@ exports.initialize_kitchen_sink_stuff = function () {
exports.initialize_everything = function () {
// initialize other stuff
emojisets.initialize();
people.initialize();
scroll_bar.initialize();
message_viewport.initialize();

View File

@ -10,7 +10,6 @@
{% block customhead %}
<meta name="apple-mobile-web-app-capable" content="yes">
<link href="/static/images/logo/apple-touch-icon-precomposed.png" rel="apple-touch-icon-precomposed">
<link id="emoji-spritesheet" href="/static/generated/emoji/{{ emojiset }}-sprite.css" rel="stylesheet" type="text/css">
<style>
#app-loading {
background-color: hsl(0, 0%, 100%);

View File

@ -2,7 +2,6 @@
{% set entrypoint = "archive" %}
{% block customhead %}
<link id="emoji-spritesheet" href="/static/generated/emoji/google-sprite.css" rel="stylesheet" type="text/css">
<style>
.portico-header {
padding-bottom: 0px;

View File

@ -20,6 +20,7 @@ sys.path.append(ZULIP_PATH)
from scripts.lib.zulip_tools import generate_sha1sum_emoji
TARGET_EMOJI_DUMP = os.path.join(ZULIP_PATH, 'static', 'generated', 'emoji')
TARGET_EMOJI_STYLES = os.path.join(ZULIP_PATH, 'static', 'generated', 'emoji-styles')
EMOJI_CACHE_PATH = "/srv/zulip-emoji-cache"
EMOJI_SCRIPT_DIR_PATH = os.path.join(ZULIP_PATH, 'tools', 'setup', 'emoji')
NODE_MODULES_PATH = os.path.join(ZULIP_PATH, 'node_modules')
@ -42,7 +43,7 @@ div.emoji,
span.emoji
{
display: inline-block;
background-image: url('sheet-%(emojiset)s-64.png');
background-image: url(~emoji-datasource-%(emojiset)s/img/%(alt_name)s/sheets-256/64.png);
background-size: %(background_size)s;
background-repeat: no-repeat;
@ -54,7 +55,7 @@ span.emoji
.emoji-1f419
{
background-image: url('images-google-64/1f419.png') !important;
background-image: url(../emoji/images-google-64/1f419.png) !important;
background-position: 0%% 0%% !important;
background-size: contain !important;
}
@ -90,6 +91,16 @@ def main() -> None:
os.remove(TARGET_EMOJI_DUMP)
os.symlink(source_emoji_dump, TARGET_EMOJI_DUMP)
# These must not be symlinked so webpack can resolve module references.
os.makedirs(TARGET_EMOJI_STYLES, exist_ok=True)
to_remove = set(os.listdir(TARGET_EMOJI_STYLES))
for filename in os.listdir(source_emoji_dump):
if filename.endswith(".css"):
shutil.copy2(os.path.join(source_emoji_dump, filename), TARGET_EMOJI_STYLES)
to_remove.discard(filename)
for filename in to_remove:
os.remove(os.path.join(TARGET_EMOJI_STYLES, filename))
def get_success_stamp() -> str:
sha1_hexdigest = generate_sha1sum_emoji(ZULIP_PATH)
return os.path.join(EMOJI_CACHE_PATH, sha1_hexdigest, 'emoji', '.success-stamp')
@ -116,7 +127,8 @@ def get_square_size(emoji_data: List[Dict[str, Any]]) -> int:
def generate_sprite_css_files(cache_path: str,
emoji_data: List[Dict[str, Any]],
emojiset: str) -> None:
emojiset: str,
alt_name: str) -> None:
"""
Spritesheets are usually NxN squares.
"""
@ -197,6 +209,7 @@ def generate_sprite_css_files(cache_path: str,
SPRITE_CSS_PATH = os.path.join(cache_path, '%s-sprite.css' % (emojiset,))
with open(SPRITE_CSS_PATH, 'w') as f:
f.write(SPRITE_CSS_FILE_TEMPLATE % {'emojiset': emojiset,
'alt_name': alt_name,
'emoji_positions': emoji_positions,
'background_size': background_size,
})
@ -245,19 +258,13 @@ def setup_emoji_farms(cache_path: str, emoji_data: List[Dict[str, Any]]) -> None
for f in os.listdir(zulip_image):
shutil.copy2(os.path.join(zulip_image, f), target_emoji_farm, follow_symlinks=False)
# Copy spritesheets.
emoji_data_path = os.path.join(NODE_MODULES_PATH, 'emoji-datasource-' + emojiset)
input_sprite_sheet = os.path.join(emoji_data_path, 'img', alt_name, 'sheets-256', '64.png')
output_sprite_sheet = os.path.join(cache_path, 'sheet-%s-64.png' % (emojiset,))
shutil.copyfile(input_sprite_sheet, output_sprite_sheet)
# We hardcode octopus emoji image to Google emojiset's old
# "cute octopus" image. Copy it to the emoji farms.
input_img_file = os.path.join(EMOJI_SCRIPT_DIR_PATH, '1f419.png')
output_img_file = os.path.join(cache_path, 'images-' + emojiset + '-64', '1f419.png')
shutil.copyfile(input_img_file, output_img_file)
generate_sprite_css_files(cache_path, emoji_data, emojiset)
generate_sprite_css_files(cache_path, emoji_data, emojiset, alt_name)
# Setup standard emojisets.
for emojiset in ['google', 'twitter']:

View File

@ -29,7 +29,7 @@ with open(EMOJI_MAP_FILE) as fp:
EMOJI_MAP = ujson.load(fp)
EMOJI_IMAGE_TEMPLATE = """
<div class="emoji emoji-%(emoji_code)s %(emojiset)s" title=%(emojiset)s></div>
<img class="emoji" src="images-%(emojiset)s-64/%(emoji_code)s.png" title=%(emojiset)s>
"""
TABLE_ROW_TEMPLATE = """
@ -47,7 +47,6 @@ TABLE_ROW_TEMPLATE = """
EMOJI_LISTING_TEMPLATE = """
<html>
<head>
<link rel = "stylesheet" type = "text/css" href = "/static/generated/emoji/google-sprite.css" />
<style>
%(table_css)s
</style>

View File

@ -52,6 +52,7 @@ EXEMPT_FILES = {
'static/js/drafts.js',
'static/js/echo.js',
'static/js/emoji_picker.js',
'static/js/emojisets.js',
'static/js/favicon.js',
'static/js/feedback_widget.js',
'static/js/floating_recipient_bar.js',

View File

@ -60,9 +60,16 @@ if args.authors_not_required:
run(authors_cmd, stdout=fp, stderr=fp)
# Collect the files that we're going to serve; this creates prod-static/serve.
run(['./manage.py', 'collectstatic', '--no-default-ignore',
'--noinput', '-i', 'assets', '-i', 'html', '-i', 'js', '-i', 'styles', '-i', 'templates'],
stdout=fp, stderr=fp)
run([
'./manage.py', 'collectstatic', '--no-default-ignore',
'--noinput',
'-i', 'assets',
'-i', 'emoji-styles',
'-i', 'html',
'-i', 'js',
'-i', 'styles',
'-i', 'templates',
], stdout=fp, stderr=fp)
# Compile translation strings to generate `.mo` files.
run(['./manage.py', 'compilemessages'], stdout=fp, stderr=fp)

View File

@ -20,7 +20,8 @@
"katex/dist/katex.css",
"./static/styles/rendered_markdown.scss",
"./static/styles/zulip.scss",
"./static/styles/portico/archive.scss"
"./static/styles/portico/archive.scss",
"./static/generated/emoji-styles/google-sprite.css"
],
"billing": [
"./static/js/bundles/portico.js",

View File

@ -121,7 +121,7 @@ export default (env?: string): webpack.Configuration[] => {
use: [{
loader: 'file-loader',
options: {
name: production ? '[name].[hash].[ext]' : '[name].[ext]',
name: production ? '[name].[hash].[ext]' : '[path][name].[ext]',
outputPath: 'files/',
},
}],

View File

@ -26,4 +26,4 @@ LATEST_RELEASE_ANNOUNCEMENT = "https://blog.zulip.org/2019/12/13/zulip-2-1-relea
# historical commits sharing the same major version, in which case a
# minor version bump suffices.
PROVISION_VERSION = '73.2'
PROVISION_VERSION = '74.0'

View File

@ -927,18 +927,6 @@ class HomeTest(ZulipTestCase):
page_params = self._get_page_params(result)
self.assertEqual(page_params['default_language'], 'es')
def test_emojiset(self) -> None:
user = self.example_user("hamlet")
user.emojiset = 'text'
user.save()
self.login(user.email)
result = self._get_home_page()
self.assertEqual(result.status_code, 200)
html = result.content.decode('utf-8')
self.assertIn('google-blob-sprite.css', html)
self.assertNotIn('text-sprite.css', html)
def test_compute_show_invites_and_add_streams_admin(self) -> None:
user = self.example_user("iago")

View File

@ -305,20 +305,11 @@ def home_real(request: HttpRequest) -> HttpResponse:
csp_nonce = generate_random_token(48)
if user_profile is not None:
if user_profile.emojiset == UserProfile.TEXT_EMOJISET:
# If current emojiset is `TEXT_EMOJISET`, then fallback to
# GOOGLE_EMOJISET for picking which spritesheet's CSS to
# include (and thus how to display emojis in the emoji picker
# and composebox typeahead).
emojiset = UserProfile.GOOGLE_BLOB_EMOJISET
else:
emojiset = user_profile.emojiset
night_mode = user_profile.night_mode
is_guest = user_profile.is_guest
is_realm_admin = user_profile.is_realm_admin
show_webathena = user_profile.realm.webathena_enabled
else: # nocoverage
emojiset = UserProfile.GOOGLE_BLOB_EMOJISET
night_mode = False
is_guest = False
is_realm_admin = False
@ -328,7 +319,6 @@ def home_real(request: HttpRequest) -> HttpResponse:
response = render(request, 'zerver/app/index.html',
context={'user_profile': user_profile,
'emojiset': emojiset,
'page_params': page_params,
'csp_nonce': csp_nonce,
'show_debug':