FEATURE: reduce avatar sizes to 6 from 20 (#21319)

* FEATURE: reduce avatar sizes to 6 from 20

This PR introduces 3 changes:

1. SiteSetting.avatar_sizes, now does what is says on the tin.
previously it would introduce a large number of extra sizes, to allow for
various DPIs. Instead we now trust the admin with the size list.

2. When `avatar_sizes` changes, we ensure consistency and remove resized
avatars that are not longer allowed per site setting. This happens on the
12 hourly job and limited out of the box to 20k cleanups per cycle, given
this may reach out to AWS 20k times to remove things.

3.Our default avatar sizes are now "24|48|72|96|144|288" these sizes were
very specifically picked to limit amount of bluriness introduced by webkit.
Our avatars are already blurry due to 1px border, so this corrects old blur.

This change heavily reduces storage required by forums which simplifies
site moves and more.

Co-authored-by: David Taylor <david@taylorhq.com>
This commit is contained in:
Sam 2023-06-01 10:00:01 +10:00 committed by GitHub
parent 70c3248b0e
commit c2332d7505
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 159 additions and 95 deletions

View File

@ -20,17 +20,17 @@ export function splitString(str, separator = ",") {
export function translateSize(size) {
switch (size) {
case "tiny":
return 20;
return 24;
case "small":
return 25;
return 24;
case "medium":
return 32;
return 48;
case "large":
return 45;
return 48;
case "extra_large":
return 60;
return 96;
case "huge":
return 120;
return 144;
}
return size;
}
@ -62,11 +62,31 @@ export function avatarUrl(template, size, { customGetURL } = {}) {
if (!template) {
return "";
}
const rawSize = getRawSize(translateSize(size));
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;

View File

@ -8,7 +8,7 @@ import {
escapeExpression,
extractDomainFromUrl,
fillMissingDates,
getRawSize,
getRawAvatarSize,
inCodeBlock,
initializeDefaultHomepage,
mergeSortedLists,
@ -86,17 +86,26 @@ 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) {
let rawSize = getRawSize;
assert.blank(avatarUrl("", "tiny"), "no template returns blank");
assert.strictEqual(
avatarUrl("/fake/template/{size}.png", "tiny"),
"/fake/template/" + rawSize(20) + ".png",
"/fake/template/" + getRawAvatarSize(24) + ".png",
"simple avatar url"
);
assert.strictEqual(
avatarUrl("/fake/template/{size}.png", "large"),
"/fake/template/" + rawSize(45) + ".png",
"/fake/template/" + getRawAvatarSize(48) + ".png",
"different size"
);
@ -104,7 +113,9 @@ module("Unit | Utilities", function (hooks) {
assert.strictEqual(
avatarUrl("/fake/template/{size}.png", "large"),
"https://app-cdn.example.com/fake/template/" + rawSize(45) + ".png",
"https://app-cdn.example.com/fake/template/" +
getRawAvatarSize(48) +
".png",
"uses CDN if present"
);
});
@ -124,7 +135,7 @@ module("Unit | Utilities", function (hooks) {
let avatarTemplate = "/path/to/avatar/{size}.png";
assert.strictEqual(
avatarImg({ avatarTemplate, size: "tiny" }),
"<img loading='lazy' alt='' width='20' height='20' src='/path/to/avatar/40.png' class='avatar'>",
"<img loading='lazy' alt='' width='24' height='24' src='/path/to/avatar/48.png' class='avatar'>",
"it returns the avatar html"
);
@ -134,7 +145,7 @@ module("Unit | Utilities", function (hooks) {
size: "tiny",
title: "evilest trout",
}),
"<img loading='lazy' alt='' width='20' height='20' src='/path/to/avatar/40.png' class='avatar' title='evilest trout' aria-label='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"
);
@ -144,7 +155,7 @@ module("Unit | Utilities", function (hooks) {
size: "tiny",
extraClasses: "evil fish",
}),
"<img loading='lazy' alt='' width='20' height='20' src='/path/to/avatar/40.png' class='avatar 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"
);

View File

@ -370,7 +370,7 @@ module("Unit | Model | report", function (hooks) {
const computedUsernameLabel = usernameLabel.compute(row);
assert.strictEqual(
computedUsernameLabel.formattedValue,
"<a href='/admin/users/1/joffrey'><img loading='lazy' alt='' width='20' height='20' src='/' class='avatar' title='joffrey' aria-label='joffrey'><span class='username'>joffrey</span></a>"
"<a href='/admin/users/1/joffrey'><img loading='lazy' alt='' width='24' height='24' src='/' class='avatar' title='joffrey' aria-label='joffrey'><span class='username'>joffrey</span></a>"
);
assert.strictEqual(computedUsernameLabel.value, "joffrey");
@ -459,7 +459,7 @@ module("Unit | Model | report", function (hooks) {
const userLink = computedLabels[0].compute(row).formattedValue;
assert.strictEqual(
userLink,
"<a href='/forum/admin/users/1/joffrey'><img loading='lazy' alt='' width='20' height='20' src='/forum/' class='avatar' title='joffrey' aria-label='joffrey'><span class='username'>joffrey</span></a>"
"<a href='/forum/admin/users/1/joffrey'><img loading='lazy' alt='' width='24' height='24' src='/forum/' class='avatar' title='joffrey' aria-label='joffrey'><span class='username'>joffrey</span></a>"
);
});
});

View File

@ -150,7 +150,7 @@ class UserAvatar < ActiveRecord::Base
tempfile.close! if tempfile && tempfile.respond_to?(:close!)
end
def self.ensure_consistency!
def self.ensure_consistency!(max_optimized_avatars_to_remove: 20_000)
DB.exec <<~SQL
UPDATE user_avatars
SET gravatar_upload_id = NULL
@ -174,6 +174,29 @@ class UserAvatar < ActiveRecord::Base
up.id IS NULL
)
SQL
ids =
DB.query_single(<<~SQL, sizes: Discourse.avatar_sizes, limit: max_optimized_avatars_to_remove)
SELECT oi.id FROM user_avatars a
JOIN optimized_images oi ON oi.upload_id = a.custom_upload_id
LEFT JOIN upload_references ur ON ur.upload_id = a.custom_upload_id and ur.target_type <> 'UserAvatar'
WHERE oi.width not in (:sizes) AND oi.height not in (:sizes) AND ur.upload_id IS NULL
LIMIT :limit
SQL
warnings_reported = 0
ids.each do |id|
begin
OptimizedImage.find(id).destroy!
rescue ActiveRecord::RecordNotFound
rescue => e
if warnings_reported < 10
Discourse.warn_exception(e, message: "Failed to remove optimized image")
warnings_reported += 1
end
end
end
end
end

View File

@ -1467,9 +1467,10 @@ files:
type: url_list
client: true
avatar_sizes:
default: "20|25|32|45|60|120"
default: "24|48|72|96|144|288"
type: list
list_type: compact
client: true
external_system_avatars_enabled:
default: true
client: true

View File

@ -327,19 +327,12 @@ module Discourse
@anonymous_top_menu_items ||= Discourse.anonymous_filters + %i[categories top]
end
# list of pixel ratios Discourse tries to optimize for
PIXEL_RATIOS ||= [1, 1.5, 2, 3]
def self.avatar_sizes
# TODO: should cache these when we get a notification system for site settings
set = Set.new
SiteSetting
.avatar_sizes
.split("|")
.map(&:to_i)
.each { |size| PIXEL_RATIOS.each { |pixel_ratio| set << (size * pixel_ratio).to_i } }
set
Set.new(SiteSetting.avatar_sizes.split("|").map(&:to_i))
end
def self.activate_plugins!

View File

@ -208,6 +208,7 @@ module PrettyText
__optInput.watchedWordsReplace = #{WordWatcher.word_matcher_regexps(:replace, engine: :js).to_json};
__optInput.watchedWordsLink = #{WordWatcher.word_matcher_regexps(:link, engine: :js).to_json};
__optInput.additionalOptions = #{Site.markdown_additional_options.to_json};
__optInput.avatar_sizes = #{SiteSetting.avatar_sizes.to_json};
JS
buffer << "__optInput.topicId = #{opts[:topic_id].to_i};\n" if opts[:topic_id]

View File

@ -16,10 +16,11 @@ define("I18n", ["exports"], function (exports) {
exports.default = I18n;
});
// Formatting doesn't currently need any helper context
define("discourse-common/lib/helpers", ["exports"], function (exports) {
exports.helperContext = function () {
return {};
return {
siteSettings: { avatar_sizes: __optInput.avatar_sizes },
};
};
});

View File

@ -237,7 +237,7 @@ martin</div>
<div class="chat-transcript" data-message-id="#{message1.id}" data-username="#{message1.user.username}" data-datetime="#{message1.created_at.iso8601}" data-channel-name="#{channel.name}" data-channel-id="#{channel.id}">
<div class="chat-transcript-user">
<div class="chat-transcript-user-avatar">
<img loading="lazy" alt="" width="20" height="20" src="//test.localhost#{post.user.avatar_template.gsub("{size}", "40")}" class="avatar">
<img loading="lazy" alt="" width="24" height="24" src="//test.localhost#{post.user.avatar_template.gsub("{size}", "48")}" class="avatar">
</div>
<div class="chat-transcript-username">
#{message1.user.username}</div>
@ -251,7 +251,7 @@ martin</div>
<div class="chat-transcript" data-message-id="#{message2.id}" data-username="#{message2.user.username}" data-datetime="#{message2.created_at.iso8601}" data-channel-name="#{channel.name}" data-channel-id="#{channel.id}">
<div class="chat-transcript-user">
<div class="chat-transcript-user-avatar">
<img loading="lazy" alt="" width="20" height="20" src="//test.localhost#{post.user.avatar_template.gsub("{size}", "40")}" class="avatar">
<img loading="lazy" alt="" width="24" height="24" src="//test.localhost#{post.user.avatar_template.gsub("{size}", "48")}" class="avatar">
</div>
<div class="chat-transcript-username">
#{message2.user.username}</div>

View File

@ -75,7 +75,7 @@ describe Chat::Message do
post = Fabricate(:post, topic: topic)
SiteSetting.external_system_avatars_enabled = false
avatar_src =
"//test.localhost#{User.system_avatar_template(post.user.username).gsub("{size}", "40")}"
"//test.localhost#{User.system_avatar_template(post.user.username).gsub("{size}", "48")}"
cooked = described_class.cook(<<~RAW)
[quote="#{post.user.username}, post:#{post.post_number}, topic:#{topic.id}"]
@ -87,7 +87,7 @@ describe Chat::Message do
<aside class="quote no-group" data-username="#{post.user.username}" data-post="#{post.post_number}" data-topic="#{topic.id}">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="20" height="20" src="#{avatar_src}" class="avatar"><a href="http://test.localhost/t/some-quotable-topic/#{topic.id}/#{post.post_number}">#{topic.title}</a>
<img loading="lazy" alt="" width="24" height="24" src="#{avatar_src}" class="avatar"><a href="http://test.localhost/t/some-quotable-topic/#{topic.id}/#{post.post_number}">#{topic.title}</a>
</div>
<blockquote>
<p>Mark me...this will go down in history.</p>
@ -101,9 +101,9 @@ describe Chat::Message do
user = Fabricate(:user, username: "chatbbcodeuser")
user2 = Fabricate(:user, username: "otherbbcodeuser")
avatar_src =
"//test.localhost#{User.system_avatar_template(user.username).gsub("{size}", "40")}"
"//test.localhost#{User.system_avatar_template(user.username).gsub("{size}", "48")}"
avatar_src2 =
"//test.localhost#{User.system_avatar_template(user2.username).gsub("{size}", "40")}"
"//test.localhost#{User.system_avatar_template(user2.username).gsub("{size}", "48")}"
msg1 =
Fabricate(
:chat_message,
@ -135,7 +135,7 @@ describe Chat::Message do
</div>
<div class="chat-transcript-user">
<div class="chat-transcript-user-avatar">
<img loading="lazy" alt="" width="20" height="20" src="#{avatar_src}" class="avatar">
<img loading="lazy" alt="" width="24" height="24" src="#{avatar_src}" class="avatar">
</div>
<div class="chat-transcript-username">
chatbbcodeuser</div>
@ -150,7 +150,7 @@ describe Chat::Message do
<div class="chat-transcript chat-transcript-chained" data-message-id="#{msg2.id}" data-username="otherbbcodeuser" data-datetime="#{msg2.created_at.iso8601}">
<div class="chat-transcript-user">
<div class="chat-transcript-user-avatar">
<img loading="lazy" alt="" width="20" height="20" src="#{avatar_src2}" class="avatar">
<img loading="lazy" alt="" width="24" height="24" src="#{avatar_src2}" class="avatar">
</div>
<div class="chat-transcript-username">
otherbbcodeuser</div>

View File

@ -13,28 +13,8 @@ RSpec.describe Discourse do
describe "avatar_sizes" do
it "returns a list of integers" do
expect(Discourse.avatar_sizes).to contain_exactly(
20,
25,
30,
32,
37,
40,
45,
48,
50,
60,
64,
67,
75,
90,
96,
120,
135,
180,
240,
360,
)
SiteSetting.avatar_sizes = "10|20|30"
expect(Discourse.avatar_sizes).to contain_exactly(10, 20, 30)
end
end

View File

@ -94,7 +94,7 @@ RSpec.describe Oneboxer do
onebox = preview(public_reply.url, user, public_category, public_topic)
expect(onebox).not_to include(public_topic.title)
expect(onebox).to include(replier.avatar_template_url.sub("{size}", "40"))
expect(onebox).to include(replier.avatar_template_url.sub("{size}", "48"))
expect(preview(public_hidden.url, user, public_category)).to match_html(
link(public_hidden.url),

View File

@ -251,7 +251,7 @@ RSpec.describe PrettyText do
<aside class="quote no-group" data-username="#{user.username}" data-post="123" data-topic="456" data-full="true">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"> #{user.username}:</div>
<img loading="lazy" alt="" width="24" height="24" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/48.png" class="avatar"> #{user.username}:</div>
<blockquote>
<p>ddd</p>
</blockquote>
@ -273,7 +273,7 @@ RSpec.describe PrettyText do
<aside class="quote no-group" data-username="#{user.username}" data-post="123" data-topic="456" data-full="true">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"> #{user.username}:</div>
<img loading="lazy" alt="" width="24" height="24" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/48.png" class="avatar"> #{user.username}:</div>
<blockquote>
<p>ddd</p>
</blockquote>
@ -294,7 +294,7 @@ RSpec.describe PrettyText do
<aside class="quote no-group" data-username="#{user.username}" data-post="555" data-topic="666">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"> #{user.username}:</div>
<img loading="lazy" alt="" width="24" height="24" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/48.png" class="avatar"> #{user.username}:</div>
<blockquote>
<p>ddd</p>
</blockquote>
@ -320,7 +320,7 @@ RSpec.describe PrettyText do
<aside class="quote group-#{group.name}" data-username="#{user.username}" data-post="2" data-topic="#{topic.id}">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="20" height="20" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png" class="avatar"><a href="http://test.localhost/t/this-is-a-test-topic/#{topic.id}/2">This is a test topic</a>
<img loading="lazy" alt="" width="24" height="24" src="//test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/48.png" class="avatar"><a href="http://test.localhost/t/this-is-a-test-topic/#{topic.id}/2">This is a test topic</a>
</div>
<blockquote>
<p>ddd</p>

View File

@ -44,11 +44,17 @@ RSpec.describe Badge do
end
it "can ensure consistency" do
b = Badge.first
b = Badge.find_by_name("Basic User")
b.grant_count = 100
b.save
UserBadge.create!(user_id: -100, badge_id: b.id, granted_at: 1.minute.ago, granted_by_id: -1)
UserBadge.create!(
user_id: User.minimum(:id) - 1,
badge_id: b.id,
granted_at: 1.minute.ago,
granted_by_id: -1,
)
UserBadge.create!(
user_id: User.first.id,
badge_id: b.id,

View File

@ -95,7 +95,7 @@ RSpec.describe UserAvatar do
stub_request(
:get,
"https://www.gravatar.com/avatar/#{avatar.user.email_hash}.png?d=404&reset_cache=5555&s=360",
"https://www.gravatar.com/avatar/#{avatar.user.email_hash}.png?d=404&reset_cache=5555&s=#{Discourse.avatar_sizes.max}",
).to_return(status: 404, body: "", headers: {})
expect do avatar.update_gravatar! end.to_not change { Upload.count }
@ -184,12 +184,39 @@ RSpec.describe UserAvatar do
end
describe "ensure_consistency!" do
it "cleans up incorrectly sized avatars" do
SiteSetting.avatar_sizes = "10|20|30"
upload = Fabricate(:upload)
user_avatar = Fabricate(:user).user_avatar
user_avatar.update_columns(custom_upload_id: upload.id)
Fabricate(:optimized_image, upload: upload, width: 10, height: 10)
Fabricate(:optimized_image, upload: upload, width: 15, height: 15)
Fabricate(:optimized_image, upload: upload, width: 20, height: 20)
UserAvatar.ensure_consistency!
expect(OptimizedImage.where(upload_id: upload.id).pluck(:width, :height).sort).to eq(
[[10, 10], [20, 20]],
)
# will not clean up if referenced
Fabricate(:optimized_image, upload: upload, width: 15, height: 15)
UploadReference.create!(upload: upload, target: Fabricate(:post))
UserAvatar.ensure_consistency!
expect(OptimizedImage.where(upload_id: upload.id).pluck(:width, :height).sort).to eq(
[[10, 10], [15, 15], [20, 20]],
)
end
it "will clean up dangling avatars" do
upload1 = Fabricate(:upload)
upload2 = Fabricate(:upload)
user_avatar = Fabricate(:user).user_avatar
user_avatar.update_columns(gravatar_upload_id: upload1.id, custom_upload_id: upload2.id)
upload1.destroy!

View File

@ -142,7 +142,8 @@ RSpec.describe User do
end
it "should not create any sidebar section link records for non human users" do
user = Fabricate(:user, id: -Time.now.to_i)
id = -Time.now.to_i
user = Fabricate(:user, id: id)
expect(SidebarSectionLink.exists?(user: user)).to eq(false)
end
@ -1359,7 +1360,7 @@ RSpec.describe User do
describe "after 3 days" do
it "should log a second visited_at record when we log an update later" do
user.update_last_seen!
future_date = freeze_time(3.days.from_now)
freeze_time(3.days.from_now)
user.update_last_seen!
expect(user.user_visits.count).to eq(2)
@ -2349,15 +2350,15 @@ RSpec.describe User do
end
it "has the correct counts" do
notification = Fabricate(:notification, user: user)
notification2 = Fabricate(:notification, user: user, read: true)
notification3 =
_notification = Fabricate(:notification, user: user)
_notification2 = Fabricate(:notification, user: user, read: true)
_notification3 =
Fabricate(
:notification,
user: user,
notification_type: Notification.types[:private_message],
)
notification4 =
_notification4 =
Fabricate(
:notification,
user: user,
@ -2377,8 +2378,8 @@ RSpec.describe User do
end
it "does not publish to the /notification channel for users who have not been seen in > 30 days" do
notification = Fabricate(:notification, user: user)
notification2 = Fabricate(:notification, user: user, read: true)
_notification = Fabricate(:notification, user: user)
_notification2 = Fabricate(:notification, user: user, read: true)
user.update(last_seen_at: 31.days.ago)
message =
@ -2934,7 +2935,7 @@ RSpec.describe User do
it "only includes enabled totp 2FA" do
enabled_totp_2fa =
Fabricate(:user_second_factor_totp, user: user, name: "Enabled TOTP", enabled: true)
disabled_totp_2fa =
_disabled_totp_2fa =
Fabricate(:user_second_factor_totp, user: user, name: "Disabled TOTP", enabled: false)
expect(user.totps.map(&:id)).to eq([enabled_totp_2fa.id])
@ -2950,7 +2951,7 @@ RSpec.describe User do
name: "Enabled YubiKey",
enabled: true,
)
disabled_security_key_2fa =
_disabled_security_key_2fa =
Fabricate(
:user_security_key_with_random_credential,
user: user,
@ -3371,7 +3372,7 @@ RSpec.describe User do
last_notification = Fabricate(:notification, user: user)
deleted_notification = Fabricate(:notification, user: user)
deleted_notification.topic.trash!
someone_else_notification = Fabricate(:notification, user: Fabricate(:user))
_someone_else_notification = Fabricate(:notification, user: Fabricate(:user))
expect(user.bump_last_seen_notification!).to eq(true)
expect(user.reload.seen_notification_id).to eq(last_notification.id)

View File

@ -462,7 +462,7 @@ RSpec.describe "users" do
before do
stub_request(
:get,
%r{https://www.gravatar.com/avatar/\w+.png\?d=404&reset_cache=\S+&s=360},
%r{https://www.gravatar.com/avatar/\w+.png\?d=404&reset_cache=\S+&s=#{Discourse.avatar_sizes.max}},
).with(
headers: {
"Accept" => "*/*",

View File

@ -60,7 +60,7 @@ RSpec.describe UserAvatarsController do
it "handles non local content correctly" do
setup_s3
SiteSetting.avatar_sizes = "100|49"
SiteSetting.avatar_sizes = "100|98|49"
SiteSetting.unicode_usernames = true
SiteSetting.s3_cdn_url = "http://cdn.com"
@ -111,7 +111,7 @@ RSpec.describe UserAvatarsController do
it "redirects to external store when enabled" do
global_setting :redirect_avatar_requests, true
setup_s3
SiteSetting.avatar_sizes = "100|49"
SiteSetting.avatar_sizes = "100|98|49"
SiteSetting.s3_cdn_url = "https://s3-cdn.example.com"
set_cdn_url("https://app-cdn.example.com")

View File

@ -162,12 +162,12 @@ RSpec.describe UserAnonymizer do
[/quote]
RAW
old_avatar_url = user.avatar_template.gsub("{size}", "40")
old_avatar_url = user.avatar_template.gsub("{size}", "48")
expect(post.cooked).to include(old_avatar_url)
make_anonymous
post.reload
new_avatar_url = user.reload.avatar_template.gsub("{size}", "40")
new_avatar_url = user.reload.avatar_template.gsub("{size}", "48")
expect(post.cooked).to_not include(old_avatar_url)
expect(post.cooked).to include(new_avatar_url)

View File

@ -425,7 +425,7 @@ RSpec.describe UsernameChanger do
let(:quoted_post) do
create_post(user: user, topic: topic, post_number: 1, raw: "quoted post")
end
let(:avatar_url) { user.avatar_template_url.gsub("{size}", "40") }
let(:avatar_url) { user.avatar_template_url.gsub("{size}", "48") }
it "replaces the username in quote tags and updates avatar" do
post = create_post_and_change_username(raw: <<~RAW)
@ -469,7 +469,7 @@ RSpec.describe UsernameChanger do
<aside class="quote no-group" data-username="bar" data-post="1" data-topic="#{quoted_post.topic.id}">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt='' width="20" height="20" src="#{avatar_url}" class="avatar"> bar:</div>
<img loading="lazy" alt='' width="24" height="24" src="#{avatar_url}" class="avatar"> bar:</div>
<blockquote>
<p>quoted post</p>
</blockquote>
@ -477,7 +477,7 @@ RSpec.describe UsernameChanger do
<aside class="quote no-group" data-username="bar">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="20" height="20" src="#{avatar_url}" class="avatar"> bar:</div>
<img loading="lazy" alt="" width="24" height="24" src="#{avatar_url}" class="avatar"> bar:</div>
<blockquote>
<p>quoted post</p>
</blockquote>
@ -485,7 +485,7 @@ RSpec.describe UsernameChanger do
<aside class="quote no-group" data-username="bar" data-post="1" data-topic="#{quoted_post.topic.id}">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="20" height="20" src="#{avatar_url}" class="avatar"> bar:</div>
<img loading="lazy" alt="" width="24" height="24" src="#{avatar_url}" class="avatar"> bar:</div>
<blockquote>
<p>quoted post</p>
</blockquote>
@ -516,7 +516,7 @@ RSpec.describe UsernameChanger do
<aside class="quote no-group" data-username="bar" data-post="1" data-topic="#{quoted_post.topic.id}">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt='' width="20" height="20" src="#{avatar_url}" class="avatar"> bar:</div>
<img loading="lazy" alt='' width="24" height="24" src="#{avatar_url}" class="avatar"> bar:</div>
<blockquote>
<p>quoted</p>
</blockquote>
@ -550,7 +550,7 @@ RSpec.describe UsernameChanger do
end
def user_avatar_url(u)
u.avatar_template_url.gsub("{size}", "40")
u.avatar_template_url.gsub("{size}", "48")
end
it "updates avatar for linked topics and posts" do
@ -562,7 +562,7 @@ RSpec.describe UsernameChanger do
<aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="20" height="20" src="#{avatar_url}" class="avatar">
<img loading="lazy" alt="" width="24" height="24" src="#{avatar_url}" class="avatar">
<a href="#{protocol_relative_url(quoted_post.full_url)}">#{quoted_post.topic.title}</a>
</div>
<blockquote>
@ -573,7 +573,7 @@ RSpec.describe UsernameChanger do
<aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="20" height="20" src="#{avatar_url}" class="avatar">
<img loading="lazy" alt="" width="24" height="24" src="#{avatar_url}" class="avatar">
<a href="#{protocol_relative_url(quoted_post.topic.url)}">#{quoted_post.topic.title}</a>
</div>
<blockquote>
@ -592,7 +592,7 @@ RSpec.describe UsernameChanger do
<aside class="quote" data-post="#{quoted_post.post_number}" data-topic="#{quoted_post.topic.id}">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="20" height="20" src="#{avatar_url}" class="avatar">
<img loading="lazy" alt="" width="24" height="24" src="#{avatar_url}" class="avatar">
<a href="#{protocol_relative_url(quoted_post.full_url)}">#{quoted_post.topic.title}</a>
</div>
<blockquote>
@ -603,7 +603,7 @@ RSpec.describe UsernameChanger do
<aside class="quote" data-post="#{another_quoted_post.post_number}" data-topic="#{another_quoted_post.topic.id}">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="20" height="20" src="#{user_avatar_url(evil_trout)}" class="avatar">
<img loading="lazy" alt="" width="24" height="24" src="#{user_avatar_url(evil_trout)}" class="avatar">
<a href="#{protocol_relative_url(another_quoted_post.full_url)}">#{another_quoted_post.topic.title}</a>
</div>
<blockquote>