Skip to content

Commit 8171270

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 8171270

File tree

9 files changed

+92
-42
lines changed

9 files changed

+92
-42
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: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ def avatar_background_color(user)
1111
end
1212

1313
def avatar_tag(user, hidden_for_screen_reader: false, **options)
14-
link_to user_path(user), class: class_names("avatar btn btn--circle", options.delete(:class)), data: { turbo_frame: "_top" },
14+
link_to user_path(user), class: class_names("avatar btn btn--circle", options.delete(:class)),
15+
data: { turbo_frame: "_top", creator_id: user.id },
1516
aria: { hidden: hidden_for_screen_reader, label: user.name },
1617
tabindex: hidden_for_screen_reader ? -1 : nil,
1718
**options do
@@ -31,13 +32,19 @@ def mail_avatar_tag(user, size: 48, **options)
3132

3233
def avatar_preview_tag(user, hidden_for_screen_reader: false, **options)
3334
tag.span class: class_names("avatar", options.delete(:class)),
35+
data: { creator_id: user.id },
3436
aria: { hidden: hidden_for_screen_reader, label: user.name },
3537
tabindex: hidden_for_screen_reader ? -1 : nil do
3638
avatar_image_tag(user, **options)
3739
end
3840
end
3941

4042
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
43+
tag.span data: { creator_id: user.id } do
44+
safe_join [
45+
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),
46+
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)
47+
]
48+
end
4249
end
4350
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)