diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index a6a3446ca..7ef6b3f89 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -679,11 +679,16 @@ overview = Overview block = Block unblock = Unblock user_bio = Biography -disabled_public_activity = This user has disabled the public visibility of the activity. email_visibility.limited = Your email address is visible to all authenticated users show_on_map = Show this place on a map settings = User settings +disabled_public_activity = This user has disabled the public visibility of the activity. +public_activity.visibility_hint.self_public = Your activity is visible to everyone, except for interactions in private spaces. Configure. +public_activity.visibility_hint.admin_public = This activity is visible to everyone, but as an administrator you can also see interactions in private spaces. +public_activity.visibility_hint.self_private = Your activity is only visible to you and the instance administrators. Configure. +public_activity.visibility_hint.admin_private = This activity is visible to you because you're an administrator, but the user wants it to remain private. + form.name_reserved = The username "%s" is reserved. form.name_pattern_not_allowed = The pattern "%s" is not allowed in a username. form.name_chars_not_allowed = Username "%s" contains invalid characters. diff --git a/release-notes/8.0.0/4189.md b/release-notes/8.0.0/4189.md new file mode 100644 index 000000000..214a10496 --- /dev/null +++ b/release-notes/8.0.0/4189.md @@ -0,0 +1 @@ +User profiles: only show RSS feed button and Public activity tab when the activity can be accessed, add messages about visibility diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl index 8590c712c..3063aeacf 100644 --- a/templates/shared/user/profile_big_avatar.tmpl +++ b/templates/shared/user/profile_big_avatar.tmpl @@ -20,7 +20,7 @@ {{end}}
{{svg "octicon-people" 18 "tw-mr-1"}}{{ctx.Locale.TrN .NumFollowers "user.followers_one" "user.followers_few" .NumFollowers}} ยท {{ctx.Locale.TrN .NumFollowing "user.following_one" "user.following_few" .NumFollowing}} - {{if .EnableFeed}} + {{if and .EnableFeed (or .IsAdmin (eq .SignedUserID .ContextUser.ID) (not .ContextUser.KeepActivityPrivate))}} {{svg "octicon-rss" 18}} {{end}}
diff --git a/templates/user/overview/header.tmpl b/templates/user/overview/header.tmpl index 1ec83042a..27568c311 100644 --- a/templates/user/overview/header.tmpl +++ b/templates/user/overview/header.tmpl @@ -32,9 +32,11 @@ {{end}} {{if .ContextUser.IsIndividual}} - - {{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}} - + {{if or (eq .TabName "activity") .IsAdmin (eq .SignedUserID .ContextUser.ID) (not .ContextUser.KeepActivityPrivate)}} + + {{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}} + + {{end}} {{if not .DisableStars}} {{svg "octicon-star"}} {{ctx.Locale.Tr "user.starred"}} diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl index 51b7a4f0e..477b838b0 100644 --- a/templates/user/profile.tmpl +++ b/templates/user/profile.tmpl @@ -9,13 +9,33 @@
{{template "user/overview/header" .}} {{if eq .TabName "activity"}} - {{if .ContextUser.KeepActivityPrivate}} -
-

{{ctx.Locale.Tr "user.disabled_public_activity"}}

+ {{if eq .SignedUserID .ContextUser.ID}} +

+ {{if .ContextUser.KeepActivityPrivate}} + {{ctx.Locale.Tr "user.public_activity.visibility_hint.self_private" "/user/settings#keep-activity-private"}} + {{else}} + {{ctx.Locale.Tr "user.public_activity.visibility_hint.self_public" "/user/settings#keep-activity-private"}} + {{end}} +

+ {{else}} + {{if .IsAdmin}} +
+ {{if .ContextUser.KeepActivityPrivate}} + {{ctx.Locale.Tr "user.public_activity.visibility_hint.admin_private"}} + {{else}} + {{ctx.Locale.Tr "user.public_activity.visibility_hint.admin_public"}} + {{end}}
+ {{else}} + {{if .ContextUser.KeepActivityPrivate}} +

{{ctx.Locale.Tr "user.disabled_public_activity"}}

+ {{end}} + {{end}} + {{end}} + {{if or .IsAdmin (eq .SignedUserID .ContextUser.ID) (not .ContextUser.KeepActivityPrivate)}} + {{template "user/heatmap" .}} + {{template "user/dashboard/feeds" .}} {{end}} - {{template "user/heatmap" .}} - {{template "user/dashboard/feeds" .}} {{else if eq .TabName "stars"}}
{{template "shared/repo_search" .}} diff --git a/tests/integration/user_profile_activity_test.go b/tests/integration/user_profile_activity_test.go new file mode 100644 index 000000000..c5485db11 --- /dev/null +++ b/tests/integration/user_profile_activity_test.go @@ -0,0 +1,97 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestUserProfileActivity ensures visibility and correctness of elements related to activity of a user: +// - RSS feed button (doesn't test `other.ENABLE_FEED:false`) +// - Public activity tab +// - Banner/hint in the tab +// - "Configure" link in the hint +func TestUserProfileActivity(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + // This test needs multiple users with different access statuses to check for all possible states + userAdmin := loginUser(t, "user1") + userRegular := loginUser(t, "user2") + // Activity availability should be the same for guest and another non-admin user, so this is not tested separately + userGuest := emptyTestSession(t) + + // The hint may contain "Configure" link with an anchor. Verify that it works. + response := userRegular.MakeRequest(t, NewRequest(t, "GET", "/user/settings"), http.StatusOK) + page := NewHTMLParser(t, response.Body) + assert.True(t, page.Find(".checkbox#keep-activity-private").Length() > 0) + + // = Public = + + // Set activity visibility of user2 to public. This is the default, but won't hurt to set it before testing. + testChangeUserActivityVisibility(t, userRegular, "off") + + // Verify availability of RSS button and activity tab + testUser2ActivityButtonsAvailability(t, userAdmin, true) + testUser2ActivityButtonsAvailability(t, userRegular, true) + testUser2ActivityButtonsAvailability(t, userGuest, true) + + // Verify the hint for all types of users: admin, self, guest + testUser2ActivityVisibility(t, userAdmin, "This activity is visible to everyone, but as an administrator you can also see interactions in private spaces.", true) + testUser2ActivityVisibility(t, userRegular, "Your activity is visible to everyone, except for interactions in private spaces. Configure.", true) + testUser2ActivityVisibility(t, userGuest, "", true) + + // = Private = + + // Set activity visibility of user2 to private + testChangeUserActivityVisibility(t, userRegular, "on") + + // Verify availability of RSS button and activity tab + testUser2ActivityButtonsAvailability(t, userAdmin, true) + testUser2ActivityButtonsAvailability(t, userRegular, true) + testUser2ActivityButtonsAvailability(t, userGuest, false) + + // Verify the hint for all types of users: admin, self, guest + testUser2ActivityVisibility(t, userAdmin, "This activity is visible to you because you're an administrator, but the user wants it to remain private.", true) + testUser2ActivityVisibility(t, userRegular, "Your activity is only visible to you and the instance administrators. Configure.", true) + testUser2ActivityVisibility(t, userGuest, "This user has disabled the public visibility of the activity.", false) + }) +} + +// testChangeUserActivityVisibility allows to easily change visibility of public activity for a user +func testChangeUserActivityVisibility(t *testing.T, session *TestSession, newState string) { + t.Helper() + session.MakeRequest(t, NewRequestWithValues(t, "POST", "/user/settings", + map[string]string{ + "_csrf": GetCSRF(t, session, "/user/settings"), + "keep_activity_private": newState, + }), http.StatusSeeOther) +} + +// testUser2ActivityVisibility checks visibility of UI elements on /?tab=activity +func testUser2ActivityVisibility(t *testing.T, session *TestSession, hint string, availability bool) { + response := session.MakeRequest(t, NewRequest(t, "GET", "/user2?tab=activity"), http.StatusOK) + page := NewHTMLParser(t, response.Body) + // Check hint visibility and correctness + testSelectorEquals(t, page, "#visibility-hint", hint) + + // Check that the hint aligns with the actual feed availability + assert.EqualValues(t, availability, page.Find("#activity-feed").Length() > 0) + + // Check availability of RSS feed button too + assert.EqualValues(t, availability, page.Find("#profile-avatar-card a[href='/user2.rss']").Length() > 0) + + // Check that the current tab is displayed and is active regardless of it's actual availability + // For example, on / it wouldn't be available to guest, but it should be still present on /?tab=activity + assert.True(t, page.Find("overflow-menu .active.item[href='/user2?tab=activity']").Length() > 0) +} + +// testUser2ActivityButtonsAvailability check visibility of Public activity tab on main profile page +func testUser2ActivityButtonsAvailability(t *testing.T, session *TestSession, buttons bool) { + response := session.MakeRequest(t, NewRequest(t, "GET", "/user2"), http.StatusOK) + page := NewHTMLParser(t, response.Body) + assert.EqualValues(t, buttons, page.Find("overflow-menu .item[href='/user2?tab=activity']").Length() > 0) +}