diff --git a/services/markup/processorhelper.go b/services/markup/processorhelper.go
index 2b1cac2a5b..5042884e5e 100644
--- a/services/markup/processorhelper.go
+++ b/services/markup/processorhelper.go
@@ -8,22 +8,26 @@ import (
 	"context"
 
 	"code.gitea.io/gitea/models/user"
-	"code.gitea.io/gitea/modules/log"
+	gitea_context "code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/markup"
 )
 
 func ProcessorHelper() *markup.ProcessorHelper {
 	return &markup.ProcessorHelper{
 		IsUsernameMentionable: func(ctx context.Context, username string) bool {
-			// TODO: cast ctx to modules/context.Context and use IsUserVisibleToViewer
-
-			// Only link if the user actually exists
-			userExists, err := user.IsUserExist(ctx, 0, username)
+			mentionedUser, err := user.GetUserByName(ctx, username)
 			if err != nil {
-				log.Error("Failed to validate user in mention %q exists, assuming it does", username)
-				userExists = true
+				return false
 			}
-			return userExists
+
+			giteaCtx, ok := ctx.(*gitea_context.Context)
+			if !ok {
+				// when using general context, use user's visibility to check
+				return mentionedUser.Visibility.IsPublic()
+			}
+
+			// when using gitea context (web context), use user's visibility and user's permission to check
+			return user.IsUserVisibleToViewer(giteaCtx, mentionedUser, giteaCtx.Doer)
 		},
 	}
 }
diff --git a/services/markup/processorhelper_test.go b/services/markup/processorhelper_test.go
index 386465bc91..f7eab3d958 100644
--- a/services/markup/processorhelper_test.go
+++ b/services/markup/processorhelper_test.go
@@ -6,15 +6,48 @@ package markup
 
 import (
 	"context"
+	"net/http"
 	"testing"
 
+	"code.gitea.io/gitea/models/db"
 	"code.gitea.io/gitea/models/unittest"
+	"code.gitea.io/gitea/models/user"
+	gitea_context "code.gitea.io/gitea/modules/context"
 
 	"github.com/stretchr/testify/assert"
 )
 
 func TestProcessorHelper(t *testing.T) {
 	assert.NoError(t, unittest.PrepareTestDatabase())
-	assert.True(t, ProcessorHelper().IsUsernameMentionable(context.Background(), "user10"))
-	assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), "no-such-user"))
+
+	userPublic := "user1"
+	userPrivate := "user31"
+	userLimited := "user33"
+	userNoSuch := "no-such-user"
+
+	unittest.AssertCount(t, &user.User{Name: userPublic}, 1)
+	unittest.AssertCount(t, &user.User{Name: userPrivate}, 1)
+	unittest.AssertCount(t, &user.User{Name: userLimited}, 1)
+	unittest.AssertCount(t, &user.User{Name: userNoSuch}, 0)
+
+	// when using general context, use user's visibility to check
+	assert.True(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPublic))
+	assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userLimited))
+	assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userPrivate))
+	assert.False(t, ProcessorHelper().IsUsernameMentionable(context.Background(), userNoSuch))
+
+	// when using web context, use user.IsUserVisibleToViewer to check
+	var err error
+	giteaCtx := &gitea_context.Context{}
+	giteaCtx.Req, err = http.NewRequest("GET", "/", nil)
+	assert.NoError(t, err)
+
+	giteaCtx.Doer = nil
+	assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic))
+	assert.False(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate))
+
+	giteaCtx.Doer, err = user.GetUserByName(db.DefaultContext, userPrivate)
+	assert.NoError(t, err)
+	assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPublic))
+	assert.True(t, ProcessorHelper().IsUsernameMentionable(giteaCtx, userPrivate))
 }