DEV: Move avatar-utils into dedicated discourse-common module (#22517)

These avatar-related helper functions are used in pretty-text, which currently means we load the entire `discourse/lib/utilities` module into the mini-racer when running pretty-text on the server side. This stops us adding any logic or imports to discourse/lib/utilities which may depend on other `discourse/` namespace features.

This commit moves the avatar-related utils into a dedicated module in the `discourse-common` namespace, adds backwards-compatibility shims, and updates the pretty-text config accordingly.
This commit is contained in:
David Taylor 2023-07-12 09:06:16 +01:00 committed by GitHub
parent aca0bf69ef
commit 2fde58def4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 210 additions and 191 deletions

View File

@ -0,0 +1,86 @@
import { getURLWithCDN } from "discourse-common/lib/get-url";
import { helperContext } from "discourse-common/lib/helpers";
import { escape } from "pretty-text/sanitizer";
import { deepMerge } from "discourse-common/lib/object";
let allowedSizes = null;
export function translateSize(size) {
switch (size) {
case "tiny":
return 24;
case "small":
return 24;
case "medium":
return 48;
case "large":
return 48;
case "extra_large":
return 96;
case "huge":
return 144;
}
return size;
}
export function getRawSize(size) {
const pixelRatio = window.devicePixelRatio || 1;
let rawSize = 1;
if (pixelRatio > 1.1 && pixelRatio < 2.1) {
rawSize = 2;
} else if (pixelRatio >= 2.1) {
rawSize = 3;
}
return size * rawSize;
}
export function getRawAvatarSize(size) {
allowedSizes ??= helperContext()
.siteSettings["avatar_sizes"].split("|")
.map((s) => parseInt(s, 10))
.sort((a, b) => a - b);
size = getRawSize(size);
for (let i = 0; i < allowedSizes.length; i++) {
if (allowedSizes[i] >= size) {
return allowedSizes[i];
}
}
return allowedSizes[allowedSizes.length - 1];
}
export function avatarUrl(template, size, { customGetURL } = {}) {
if (!template) {
return "";
}
const rawSize = getRawAvatarSize(translateSize(size));
const templatedPath = template.replace(/\{size\}/g, rawSize);
return (customGetURL || getURLWithCDN)(templatedPath);
}
export function avatarImg(options, customGetURL) {
const size = translateSize(options.size);
let url = avatarUrl(options.avatarTemplate, size, { customGetURL });
// We won't render an invalid url
if (!url) {
return "";
}
const classes =
"avatar" + (options.extraClasses ? " " + options.extraClasses : "");
let title = "";
if (options.title) {
const escaped = escape(options.title || "");
title = ` title='${escaped}' aria-label='${escaped}'`;
}
return `<img loading='lazy' alt='' width='${size}' height='${size}' src='${url}' class='${classes}'${title}>`;
}
export function tinyAvatar(avatarTemplate, options) {
return avatarImg(deepMerge({ avatarTemplate, size: "tiny" }, options));
}

View File

@ -6,8 +6,8 @@ import {
caretPosition, caretPosition,
formatUsername, formatUsername,
inCodeBlock, inCodeBlock,
tinyAvatar,
} from "discourse/lib/utilities"; } from "discourse/lib/utilities";
import { tinyAvatar } from "discourse-common/lib/avatar-utils";
import discourseComputed, { import discourseComputed, {
bind, bind,
debounce, debounce,

View File

@ -1,4 +1,4 @@
import { avatarImg } from "discourse/lib/utilities"; import { avatarImg } from "discourse-common/lib/avatar-utils";
import { htmlHelper } from "discourse-common/lib/helpers"; import { htmlHelper } from "discourse-common/lib/helpers";
import { isEmpty } from "@ember/utils"; import { isEmpty } from "@ember/utils";

View File

@ -1,5 +1,5 @@
import { addExtraUserClasses } from "discourse/helpers/user-avatar"; import { addExtraUserClasses } from "discourse/helpers/user-avatar";
import { avatarImg } from "discourse/lib/utilities"; import { avatarImg } from "discourse-common/lib/avatar-utils";
import { get } from "@ember/object"; import { get } from "@ember/object";
import { htmlHelper } from "discourse-common/lib/helpers"; import { htmlHelper } from "discourse-common/lib/helpers";
import { isEmpty } from "@ember/utils"; import { isEmpty } from "@ember/utils";

View File

@ -1,4 +1,5 @@
import { avatarImg, formatUsername } from "discourse/lib/utilities"; import { formatUsername } from "discourse/lib/utilities";
import { avatarImg } from "discourse-common/lib/avatar-utils";
import I18n from "I18n"; import I18n from "I18n";
import { get } from "@ember/object"; import { get } from "@ember/object";
import { htmlSafe } from "@ember/template"; import { htmlSafe } from "@ember/template";

View File

@ -1,14 +1,31 @@
import getURL, { getURLWithCDN } from "discourse-common/lib/get-url"; import getURL from "discourse-common/lib/get-url";
import Handlebars from "handlebars"; import Handlebars from "handlebars";
import I18n from "I18n"; import I18n from "I18n";
import { deepMerge } from "discourse-common/lib/object";
import { escape } from "pretty-text/sanitizer"; import { escape } from "pretty-text/sanitizer";
import { helperContext } from "discourse-common/lib/helpers"; import { helperContext } from "discourse-common/lib/helpers";
import toMarkdown from "discourse/lib/to-markdown"; import toMarkdown from "discourse/lib/to-markdown";
import deprecated from "discourse-common/lib/deprecated"; import deprecated from "discourse-common/lib/deprecated";
import * as AvatarUtils from "discourse-common/lib/avatar-utils";
let _defaultHomepage; let _defaultHomepage;
function deprecatedAvatarUtil(name) {
return function () {
deprecated(
`${name} should be imported from discourse-common/lib/avatar-utils instead of discourse/lib/utilities`,
{ id: "discourse.avatar-utils" }
);
return AvatarUtils[name](...arguments);
};
}
export const translateSize = deprecatedAvatarUtil("translateSize");
export const getRawSize = deprecatedAvatarUtil("getRawSize");
export const getRawAvatarSize = deprecatedAvatarUtil("getRawAvatarSize");
export const avatarUrl = deprecatedAvatarUtil("avatarUrl");
export const avatarImg = deprecatedAvatarUtil("avatarImg");
export const tinyAvatar = deprecatedAvatarUtil("tinyAvatar");
export function splitString(str, separator = ",") { export function splitString(str, separator = ",") {
if (typeof str === "string") { if (typeof str === "string") {
return str.split(separator).filter(Boolean); return str.split(separator).filter(Boolean);
@ -17,24 +34,6 @@ export function splitString(str, separator = ",") {
} }
} }
export function translateSize(size) {
switch (size) {
case "tiny":
return 24;
case "small":
return 24;
case "medium":
return 48;
case "large":
return 48;
case "extra_large":
return 96;
case "huge":
return 144;
}
return size;
}
export function escapeExpression(string) { export function escapeExpression(string) {
if (!string) { if (!string) {
return ""; return "";
@ -58,70 +57,6 @@ export function replaceFormatter(fn) {
_usernameFormatDelegate = fn; _usernameFormatDelegate = fn;
} }
export function avatarUrl(template, size, { customGetURL } = {}) {
if (!template) {
return "";
}
const rawSize = getRawAvatarSize(translateSize(size));
const templatedPath = template.replace(/\{size\}/g, rawSize);
return (customGetURL || getURLWithCDN)(templatedPath);
}
let allowedSizes = null;
export function getRawAvatarSize(size) {
allowedSizes ??= helperContext()
.siteSettings["avatar_sizes"].split("|")
.map((s) => parseInt(s, 10))
.sort((a, b) => a - b);
size = getRawSize(size);
for (let i = 0; i < allowedSizes.length; i++) {
if (allowedSizes[i] >= size) {
return allowedSizes[i];
}
}
return allowedSizes[allowedSizes.length - 1];
}
export function getRawSize(size) {
const pixelRatio = window.devicePixelRatio || 1;
let rawSize = 1;
if (pixelRatio > 1.1 && pixelRatio < 2.1) {
rawSize = 2;
} else if (pixelRatio >= 2.1) {
rawSize = 3;
}
return size * rawSize;
}
export function avatarImg(options, customGetURL) {
const size = translateSize(options.size);
let url = avatarUrl(options.avatarTemplate, size, { customGetURL });
// We won't render an invalid url
if (!url) {
return "";
}
const classes =
"avatar" + (options.extraClasses ? " " + options.extraClasses : "");
let title = "";
if (options.title) {
const escaped = escapeExpression(options.title || "");
title = ` title='${escaped}' aria-label='${escaped}'`;
}
return `<img loading='lazy' alt='' width='${size}' height='${size}' src='${url}' class='${classes}'${title}>`;
}
export function tinyAvatar(avatarTemplate, options) {
return avatarImg(deepMerge({ avatarTemplate, size: "tiny" }, options));
}
export function postUrl(slug, topicId, postNumber) { export function postUrl(slug, topicId, postNumber) {
let url = getURL("/t/"); let url = getURL("/t/");
if (slug) { if (slug) {
@ -659,6 +594,3 @@ export function mergeSortedLists(list1, list2, comparator) {
} }
return merged; return merged;
} }
// This prevents a mini racer crash
export default {};

View File

@ -5,11 +5,8 @@ import discourseComputed, {
observes, observes,
on, on,
} from "discourse-common/utils/decorators"; } from "discourse-common/utils/decorators";
import { import { emailValid, escapeExpression } from "discourse/lib/utilities";
emailValid, import { tinyAvatar } from "discourse-common/lib/avatar-utils";
escapeExpression,
tinyAvatar,
} from "discourse/lib/utilities";
import Draft from "discourse/models/draft"; import Draft from "discourse/models/draft";
import I18n from "I18n"; import I18n from "I18n";
import { Promise } from "rsvp"; import { Promise } from "rsvp";

View File

@ -1,9 +1,6 @@
import { applyDecorators, createWidget } from "discourse/widgets/widget"; import { applyDecorators, createWidget } from "discourse/widgets/widget";
import { import { formatUsername } from "discourse/lib/utilities";
avatarUrl, import { avatarUrl, translateSize } from "discourse-common/lib/avatar-utils";
formatUsername,
translateSize,
} from "discourse/lib/utilities";
import getURL, { getURLWithCDN } from "discourse-common/lib/get-url"; import getURL, { getURLWithCDN } from "discourse-common/lib/get-url";
import DecoratorHelper from "discourse/widgets/decorator-helper"; import DecoratorHelper from "discourse/widgets/decorator-helper";
import DiscourseURL from "discourse/lib/url"; import DiscourseURL from "discourse/lib/url";

View File

@ -0,0 +1,93 @@
import {
avatarImg,
avatarUrl,
getRawAvatarSize,
} from "discourse-common/lib/avatar-utils";
import { module, test } from "qunit";
import { setupURL } from "discourse-common/lib/get-url";
import { setupTest } from "ember-qunit";
module("Unit | Utilities", function (hooks) {
setupTest(hooks);
test("getRawAvatarSize avoids redirects", function (assert) {
assert.strictEqual(
getRawAvatarSize(1),
24,
"returns the first size larger on the menu"
);
assert.strictEqual(getRawAvatarSize(2000), 288, "caps at highest");
});
test("avatarUrl", function (assert) {
assert.blank(avatarUrl("", "tiny"), "no template returns blank");
assert.strictEqual(
avatarUrl("/fake/template/{size}.png", "tiny"),
"/fake/template/" + getRawAvatarSize(24) + ".png",
"simple avatar url"
);
assert.strictEqual(
avatarUrl("/fake/template/{size}.png", "large"),
"/fake/template/" + getRawAvatarSize(48) + ".png",
"different size"
);
setupURL("https://app-cdn.example.com", "https://example.com", "");
assert.strictEqual(
avatarUrl("/fake/template/{size}.png", "large"),
"https://app-cdn.example.com/fake/template/" +
getRawAvatarSize(48) +
".png",
"uses CDN if present"
);
});
let setDevicePixelRatio = function (value) {
if (Object.defineProperty && !window.hasOwnProperty("devicePixelRatio")) {
Object.defineProperty(window, "devicePixelRatio", { value: 2 });
} else {
window.devicePixelRatio = value;
}
};
test("avatarImg", function (assert) {
let oldRatio = window.devicePixelRatio;
setDevicePixelRatio(2);
let avatarTemplate = "/path/to/avatar/{size}.png";
assert.strictEqual(
avatarImg({ avatarTemplate, size: "tiny" }),
"<img loading='lazy' alt='' width='24' height='24' src='/path/to/avatar/48.png' class='avatar'>",
"it returns the avatar html"
);
assert.strictEqual(
avatarImg({
avatarTemplate,
size: "tiny",
title: "evilest trout",
}),
"<img loading='lazy' alt='' width='24' height='24' src='/path/to/avatar/48.png' class='avatar' title='evilest trout' aria-label='evilest trout'>",
"it adds a title if supplied"
);
assert.strictEqual(
avatarImg({
avatarTemplate,
size: "tiny",
extraClasses: "evil fish",
}),
"<img loading='lazy' alt='' width='24' height='24' src='/path/to/avatar/48.png' class='avatar evil fish'>",
"it adds extra classes if supplied"
);
assert.blank(
avatarImg({ avatarTemplate: "", size: "tiny" }),
"it doesn't render avatars for invalid avatar template"
);
setDevicePixelRatio(oldRatio);
});
});

View File

@ -1,6 +1,4 @@
import { import {
avatarImg,
avatarUrl,
caretRowCol, caretRowCol,
clipboardCopyAsync, clipboardCopyAsync,
defaultHomepage, defaultHomepage,
@ -8,7 +6,6 @@ import {
escapeExpression, escapeExpression,
extractDomainFromUrl, extractDomainFromUrl,
fillMissingDates, fillMissingDates,
getRawAvatarSize,
inCodeBlock, inCodeBlock,
initializeDefaultHomepage, initializeDefaultHomepage,
mergeSortedLists, mergeSortedLists,
@ -25,7 +22,6 @@ import { chromeTest } from "discourse/tests/helpers/qunit-helpers";
import { setupRenderingTest } from "discourse/tests/helpers/component-test"; import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { click, render } from "@ember/test-helpers"; import { click, render } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars"; import { hbs } from "ember-cli-htmlbars";
import { setupURL } from "discourse-common/lib/get-url";
import { setupTest } from "ember-qunit"; import { setupTest } from "ember-qunit";
import { getOwner } from "discourse-common/lib/get-owner"; import { getOwner } from "discourse-common/lib/get-owner";
@ -86,87 +82,6 @@ module("Unit | Utilities", function (hooks) {
); );
}); });
test("getRawAvatarSize avoids redirects", function (assert) {
assert.strictEqual(
getRawAvatarSize(1),
24,
"returns the first size larger on the menu"
);
assert.strictEqual(getRawAvatarSize(2000), 288, "caps at highest");
});
test("avatarUrl", function (assert) {
assert.blank(avatarUrl("", "tiny"), "no template returns blank");
assert.strictEqual(
avatarUrl("/fake/template/{size}.png", "tiny"),
"/fake/template/" + getRawAvatarSize(24) + ".png",
"simple avatar url"
);
assert.strictEqual(
avatarUrl("/fake/template/{size}.png", "large"),
"/fake/template/" + getRawAvatarSize(48) + ".png",
"different size"
);
setupURL("https://app-cdn.example.com", "https://example.com", "");
assert.strictEqual(
avatarUrl("/fake/template/{size}.png", "large"),
"https://app-cdn.example.com/fake/template/" +
getRawAvatarSize(48) +
".png",
"uses CDN if present"
);
});
let setDevicePixelRatio = function (value) {
if (Object.defineProperty && !window.hasOwnProperty("devicePixelRatio")) {
Object.defineProperty(window, "devicePixelRatio", { value: 2 });
} else {
window.devicePixelRatio = value;
}
};
test("avatarImg", function (assert) {
let oldRatio = window.devicePixelRatio;
setDevicePixelRatio(2);
let avatarTemplate = "/path/to/avatar/{size}.png";
assert.strictEqual(
avatarImg({ avatarTemplate, size: "tiny" }),
"<img loading='lazy' alt='' width='24' height='24' src='/path/to/avatar/48.png' class='avatar'>",
"it returns the avatar html"
);
assert.strictEqual(
avatarImg({
avatarTemplate,
size: "tiny",
title: "evilest trout",
}),
"<img loading='lazy' alt='' width='24' height='24' src='/path/to/avatar/48.png' class='avatar' title='evilest trout' aria-label='evilest trout'>",
"it adds a title if supplied"
);
assert.strictEqual(
avatarImg({
avatarTemplate,
size: "tiny",
extraClasses: "evil fish",
}),
"<img loading='lazy' alt='' width='24' height='24' src='/path/to/avatar/48.png' class='avatar evil fish'>",
"it adds extra classes if supplied"
);
assert.blank(
avatarImg({ avatarTemplate: "", size: "tiny" }),
"it doesn't render avatars for invalid avatar template"
);
setDevicePixelRatio(oldRatio);
});
test("defaultHomepage via meta tag", function (assert) { test("defaultHomepage via meta tag", function (assert) {
let meta = document.createElement("meta"); let meta = document.createElement("meta");
meta.name = "discourse_current_homepage"; meta.name = "discourse_current_homepage";

View File

@ -104,9 +104,9 @@ module PrettyText
apply_es6_file(ctx, root_path, "discourse-common/addon/lib/object") apply_es6_file(ctx, root_path, "discourse-common/addon/lib/object")
apply_es6_file(ctx, root_path, "discourse-common/addon/lib/deprecated") apply_es6_file(ctx, root_path, "discourse-common/addon/lib/deprecated")
apply_es6_file(ctx, root_path, "discourse-common/addon/lib/escape") apply_es6_file(ctx, root_path, "discourse-common/addon/lib/escape")
apply_es6_file(ctx, root_path, "discourse-common/addon/lib/avatar-utils")
apply_es6_file(ctx, root_path, "discourse-common/addon/utils/watched-words") apply_es6_file(ctx, root_path, "discourse-common/addon/utils/watched-words")
apply_es6_file(ctx, root_path, "discourse/app/lib/to-markdown") apply_es6_file(ctx, root_path, "discourse/app/lib/to-markdown")
apply_es6_file(ctx, root_path, "discourse/app/lib/utilities")
ctx.load("#{Rails.root}/lib/pretty_text/shims.js") ctx.load("#{Rails.root}/lib/pretty_text/shims.js")
ctx.eval("__setUnicode(#{Emoji.unicode_replacements_json})") ctx.eval("__setUnicode(#{Emoji.unicode_replacements_json})")
@ -260,7 +260,7 @@ module PrettyText
__optInput = {}; __optInput = {};
__optInput.avatar_sizes = #{SiteSetting.avatar_sizes.to_json}; __optInput.avatar_sizes = #{SiteSetting.avatar_sizes.to_json};
__paths = #{paths_json}; __paths = #{paths_json};
__utils.avatarImg({size: #{size.inspect}, avatarTemplate: #{avatar_template.inspect}}, __getURL); require("discourse-common/lib/avatar-utils").avatarImg({size: #{size.inspect}, avatarTemplate: #{avatar_template.inspect}}, __getURL);
JS JS
end end

View File

@ -24,8 +24,6 @@ define("discourse-common/lib/helpers", ["exports"], function (exports) {
}; };
}); });
__utils = require("discourse/lib/utilities");
__emojiUnicodeReplacer = null; __emojiUnicodeReplacer = null;
__setUnicode = function (replacements) { __setUnicode = function (replacements) {
@ -119,7 +117,7 @@ function __hashtagLookup(slug, cookingUserId, typesInPriorityOrder) {
} }
function __lookupAvatar(p) { function __lookupAvatar(p) {
return __utils.avatarImg( return require("discourse-common/lib/avatar-utils").avatarImg(
{ size: "tiny", avatarTemplate: __helpers.avatar_template(p) }, { size: "tiny", avatarTemplate: __helpers.avatar_template(p) },
__getURL __getURL
); );