Skip to content

Commit c9bc818

Browse files
Serve own avatar from its own endpoint
This allows us to have different cache controls depending on whether you're viewing your own avatar, or someone else's. Your own avatar will always be fresh, while other folks' avatars can be pulled from the CDN.
1 parent def4872 commit c9bc818

File tree

9 files changed

+89
-41
lines changed

9 files changed

+89
-41
lines changed

app/controllers/users/avatars_controller.rb

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,20 @@ class Users::AvatarsController < ApplicationController
77
before_action :ensure_permission_to_administer_user, only: :destroy
88

99
def show
10+
expires_in 30.minutes, public: true, stale_while_revalidate: 1.week
11+
1012
if @user.system?
1113
redirect_to view_context.image_path("system_user.png")
1214
elsif @user.avatar.attached?
1315
redirect_to rails_blob_url(@user.avatar.variant(:thumb), disposition: "inline")
14-
elsif stale? @user, cache_control: cache_control
16+
else
1517
render_initials
1618
end
1719
end
1820

1921
def destroy
2022
@user.avatar.destroy
23+
@user.touch
2124
redirect_to @user
2225
end
2326

@@ -30,14 +33,6 @@ def ensure_permission_to_administer_user
3033
head :forbidden unless Current.user.can_change?(@user)
3134
end
3235

33-
def cache_control
34-
if @user == Current.user
35-
{}
36-
else
37-
{ max_age: 30.minutes, stale_while_revalidate: 1.week }
38-
end
39-
end
40-
4136
def render_initials
4237
render formats: :svg
4338
end
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
class Users::MyAvatarController < ApplicationController
2+
include ActiveStorage::Streaming
3+
4+
def show
5+
if stale? Current.user
6+
if Current.user.avatar.attached?
7+
redirect_to rails_blob_url(Current.user.avatar.variant(:thumb), disposition: "inline")
8+
else
9+
render_initials
10+
end
11+
end
12+
end
13+
14+
private
15+
def render_initials
16+
render formats: :svg
17+
end
18+
end

app/helpers/avatars_helper.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ def avatar_preview_tag(user, hidden_for_screen_reader: false, **options)
3838
end
3939

4040
def avatar_image_tag(user, **options)
41-
image_tag user_avatar_url(user, script_name: user.account.slug), aria: { hidden: "true" }, size: 48, title: user.name, **options
41+
tag.span data: { creator_id: user.id } do
42+
safe_join [
43+
image_tag(user_avatar_url(user, script_name: user.account.slug), aria: { hidden: "true" }, size: 48, title: user.name, data: { only_visible_to_others: true }, **options),
44+
image_tag(my_avatar_url(script_name: user.account.slug), aria: { hidden: "true" }, size: 48, title: user.name, data: { only_visible_to_you: true }, **options)
45+
]
46+
end
4247
end
4348
end

app/views/users/_initials.svg.erb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
2+
viewBox="0 0 512 512" class="avatar" aria-hidden="true">
3+
<defs>
4+
<clipPath id="porthole">
5+
<circle cx="50%" cy="50%" r="50%" />
6+
</clipPath>
7+
</defs>
8+
9+
<g>
10+
<rect width="100%" height="100%" rx="50" fill="<%= avatar_background_color(user) %>" />
11+
12+
<text x="50%" y="50%" fill="#FFFFFF"
13+
text-anchor="middle" dy="0.35em"
14+
<%=raw 'textLength="85%" lengthAdjust="spacingAndGlyphs"' if user.initials.size >= 3 %>
15+
font-family="-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif"
16+
font-size="230"
17+
font-weight="800"
18+
letter-spacing="-5">
19+
<%= user.initials %>
20+
</text>
21+
</g>
22+
</svg>
Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1 @@
1-
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
2-
viewBox="0 0 512 512" class="avatar" aria-hidden="true">
3-
<defs>
4-
<clipPath id="porthole">
5-
<circle cx="50%" cy="50%" r="50%" />
6-
</clipPath>
7-
</defs>
8-
9-
<g>
10-
<rect width="100%" height="100%" rx="50" fill="<%= avatar_background_color(@user) %>" />
11-
12-
<text x="50%" y="50%" fill="#FFFFFF"
13-
text-anchor="middle" dy="0.35em"
14-
<%=raw 'textLength="85%" lengthAdjust="spacingAndGlyphs"' if @user.initials.size >= 3 %>
15-
font-family="-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif"
16-
font-size="230"
17-
font-weight="800"
18-
letter-spacing="-5">
19-
<%= @user.initials %>
20-
</text>
21-
</g>
22-
</svg>
1+
<%= render "users/initials", user: @user %>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<%= render "users/initials", user: Current.user %>

config/routes.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
resources :exports, only: [ :create, :show ]
99
end
1010

11+
get "users/mine/avatar", to: "users/my_avatar#show", as: :my_avatar
12+
1113
resources :users do
1214
scope module: :users do
1315
resource :avatar

test/controllers/users/avatars_controller_test.rb

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,10 @@ class Users::AvatarsControllerTest < ActionDispatch::IntegrationTest
1212
assert_redirected_to ActionController::Base.helpers.image_path("system_user.png")
1313
end
1414

15-
test "show own initials without caching" do
15+
test "show initials with public caching" do
1616
get user_avatar_path(users(:david))
1717
assert_match "image/svg+xml", @response.content_type
18-
assert @response.cache_control[:private]
19-
assert_equal "0", @response.cache_control[:max_age]
20-
end
21-
22-
test "show other initials with caching" do
23-
get user_avatar_path(users(:kevin))
24-
assert_match "image/svg+xml", @response.content_type
25-
assert @response.cache_control[:private]
18+
assert @response.cache_control[:public]
2619
assert_equal 30.minutes.to_s, @response.cache_control[:max_age]
2720
end
2821

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
require "test_helper"
2+
3+
class Users::MyAvatarControllerTest < ActionDispatch::IntegrationTest
4+
setup do
5+
sign_in_as :david
6+
end
7+
8+
test "show own initials" do
9+
get my_avatar_path
10+
assert_match "image/svg+xml", @response.content_type
11+
assert_match "private", @response.headers["Cache-Control"]
12+
assert_match "must-revalidate", @response.headers["Cache-Control"]
13+
end
14+
15+
test "show own image redirects to the blob url" do
16+
users(:david).avatar.attach(io: File.open(file_fixture("moon.jpg")), filename: "moon.jpg", content_type: "image/jpeg")
17+
assert users(:david).avatar.attached?
18+
19+
get my_avatar_path
20+
21+
assert_redirected_to rails_blob_url(users(:david).avatar.variant(:thumb), disposition: "inline")
22+
assert_match "private", @response.headers["Cache-Control"]
23+
assert_match "must-revalidate", @response.headers["Cache-Control"]
24+
end
25+
26+
test "requires authentication" do
27+
sign_out
28+
29+
get my_avatar_path
30+
31+
assert_response :redirect
32+
end
33+
end

0 commit comments

Comments
 (0)