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,
formatUsername,
inCodeBlock,
tinyAvatar,
} from "discourse/lib/utilities";
import { tinyAvatar } from "discourse-common/lib/avatar-utils";
import discourseComputed, {
bind,
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 { isEmpty } from "@ember/utils";

View File

@ -1,5 +1,5 @@
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 { htmlHelper } from "discourse-common/lib/helpers";
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 { get } from "@ember/object";
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 I18n from "I18n";
import { deepMerge } from "discourse-common/lib/object";
import { escape } from "pretty-text/sanitizer";
import { helperContext } from "discourse-common/lib/helpers";
import toMarkdown from "discourse/lib/to-markdown";
import deprecated from "discourse-common/lib/deprecated";
import * as AvatarUtils from "discourse-common/lib/avatar-utils";
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 = ",") {
if (typeof str === "string") {
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) {
if (!string) {
return "";
@ -58,70 +57,6 @@ export function replaceFormatter(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) {
let url = getURL("/t/");
if (slug) {
@ -659,6 +594,3 @@ export function mergeSortedLists(list1, list2, comparator) {
}
return merged;
}
// This prevents a mini racer crash
export default {};

View File

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

View File

@ -1,9 +1,6 @@
import { applyDecorators, createWidget } from "discourse/widgets/widget";
import {
avatarUrl,
formatUsername,
translateSize,
} from "discourse/lib/utilities";
import { formatUsername } from "discourse/lib/utilities";
import { avatarUrl, translateSize } from "discourse-common/lib/avatar-utils";
import getURL, { getURLWithCDN } from "discourse-common/lib/get-url";
import DecoratorHelper from "discourse/widgets/decorator-helper";
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 {
avatarImg,
avatarUrl,
caretRowCol,
clipboardCopyAsync,
defaultHomepage,
@ -8,7 +6,6 @@ import {
escapeExpression,
extractDomainFromUrl,
fillMissingDates,
getRawAvatarSize,
inCodeBlock,
initializeDefaultHomepage,
mergeSortedLists,
@ -25,7 +22,6 @@ import { chromeTest } from "discourse/tests/helpers/qunit-helpers";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import { click, render } from "@ember/test-helpers";
import { hbs } from "ember-cli-htmlbars";
import { setupURL } from "discourse-common/lib/get-url";
import { setupTest } from "ember-qunit";
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) {
let meta = document.createElement("meta");
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/deprecated")
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/app/lib/to-markdown")
apply_es6_file(ctx, root_path, "discourse/app/lib/utilities")
ctx.load("#{Rails.root}/lib/pretty_text/shims.js")
ctx.eval("__setUnicode(#{Emoji.unicode_replacements_json})")
@ -260,7 +260,7 @@ module PrettyText
__optInput = {};
__optInput.avatar_sizes = #{SiteSetting.avatar_sizes.to_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
end

View File

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