diff --git a/integrations/privateactivity_test.go b/integrations/privateactivity_test.go
new file mode 100644
index 0000000000..e9beb7c116
--- /dev/null
+++ b/integrations/privateactivity_test.go
@@ -0,0 +1,414 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package integrations
+
+import (
+	"fmt"
+	"net/http"
+	"testing"
+
+	"code.gitea.io/gitea/models"
+	api "code.gitea.io/gitea/modules/structs"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const privateActivityTestAdmin = "user1"
+const privateActivityTestUser = "user2"
+
+// user3 is an organization so it is not usable here
+const privateActivityTestOtherUser = "user4"
+
+// activity helpers
+
+func testPrivateActivityDoSomethingForActionEntries(t *testing.T) {
+	repoBefore := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
+	owner := models.AssertExistsAndLoadBean(t, &models.User{ID: repoBefore.OwnerID}).(*models.User)
+
+	session := loginUser(t, privateActivityTestUser)
+	token := getTokenForLoggedInUser(t, session)
+	urlStr := fmt.Sprintf("/api/v1/repos/%s/%s/issues?state=all&token=%s", owner.Name, repoBefore.Name, token)
+	req := NewRequestWithJSON(t, "POST", urlStr, &api.CreateIssueOption{
+		Body:  "test",
+		Title: "test",
+	})
+	session.MakeRequest(t, req, http.StatusCreated)
+}
+
+// private activity helpers
+
+func testPrivateActivityHelperEnablePrivateActivity(t *testing.T) {
+	session := loginUser(t, privateActivityTestUser)
+	req := NewRequestWithValues(t, "POST", "/user/settings", map[string]string{
+		"_csrf":                 GetCSRF(t, session, "/user/settings"),
+		"name":                  privateActivityTestUser,
+		"email":                 privateActivityTestUser + "@example.com",
+		"language":              "en-us",
+		"keep_activity_private": "1",
+	})
+	session.MakeRequest(t, req, http.StatusFound)
+}
+
+func testPrivateActivityHelperHasVisibleActivitiesInHTMLDoc(htmlDoc *HTMLDoc) bool {
+	return htmlDoc.doc.Find(".feeds").Find(".news").Length() > 0
+}
+
+func testPrivateActivityHelperHasVisibleActivitiesFromSession(t *testing.T, session *TestSession) bool {
+	req := NewRequestf(t, "GET", "/%s?tab=activity", privateActivityTestUser)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+
+	htmlDoc := NewHTMLParser(t, resp.Body)
+
+	return testPrivateActivityHelperHasVisibleActivitiesInHTMLDoc(htmlDoc)
+}
+
+func testPrivateActivityHelperHasVisibleActivitiesFromPublic(t *testing.T) bool {
+	req := NewRequestf(t, "GET", "/%s?tab=activity", privateActivityTestUser)
+	resp := MakeRequest(t, req, http.StatusOK)
+
+	htmlDoc := NewHTMLParser(t, resp.Body)
+
+	return testPrivateActivityHelperHasVisibleActivitiesInHTMLDoc(htmlDoc)
+}
+
+// heatmap UI helpers
+
+func testPrivateActivityHelperHasVisibleHeatmapInHTMLDoc(htmlDoc *HTMLDoc) bool {
+	return htmlDoc.doc.Find("#user-heatmap").Length() > 0
+}
+
+func testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t *testing.T, session *TestSession) bool {
+	req := NewRequestf(t, "GET", "/%s?tab=activity", privateActivityTestUser)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+
+	htmlDoc := NewHTMLParser(t, resp.Body)
+
+	return testPrivateActivityHelperHasVisibleHeatmapInHTMLDoc(htmlDoc)
+}
+
+func testPrivateActivityHelperHasVisibleDashboardHeatmapFromSession(t *testing.T, session *TestSession) bool {
+	req := NewRequest(t, "GET", "/")
+	resp := session.MakeRequest(t, req, http.StatusOK)
+
+	htmlDoc := NewHTMLParser(t, resp.Body)
+
+	return testPrivateActivityHelperHasVisibleHeatmapInHTMLDoc(htmlDoc)
+}
+
+func testPrivateActivityHelperHasVisibleHeatmapFromPublic(t *testing.T) bool {
+	req := NewRequestf(t, "GET", "/%s?tab=activity", privateActivityTestUser)
+	resp := MakeRequest(t, req, http.StatusOK)
+
+	htmlDoc := NewHTMLParser(t, resp.Body)
+
+	return testPrivateActivityHelperHasVisibleHeatmapInHTMLDoc(htmlDoc)
+}
+
+// heatmap API helpers
+
+func testPrivateActivityHelperHasHeatmapContentFromPublic(t *testing.T) bool {
+	req := NewRequestf(t, "GET", "/api/v1/users/%s/heatmap", privateActivityTestUser)
+	resp := MakeRequest(t, req, http.StatusOK)
+
+	var items []*models.UserHeatmapData
+	DecodeJSON(t, resp, &items)
+
+	return len(items) != 0
+}
+
+func testPrivateActivityHelperHasHeatmapContentFromSession(t *testing.T, session *TestSession) bool {
+	token := getTokenForLoggedInUser(t, session)
+
+	req := NewRequestf(t, "GET", "/api/v1/users/%s/heatmap?token=%s", privateActivityTestUser, token)
+	resp := session.MakeRequest(t, req, http.StatusOK)
+
+	var items []*models.UserHeatmapData
+	DecodeJSON(t, resp, &items)
+
+	return len(items) != 0
+}
+
+// check activity visibility if the visibility is enabled
+
+func TestPrivateActivityNoVisibleForPublic(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+
+	visible := testPrivateActivityHelperHasVisibleActivitiesFromPublic(t)
+
+	assert.True(t, visible, "user should have visible activities")
+}
+
+func TestPrivateActivityNoVisibleForUserItself(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+
+	session := loginUser(t, privateActivityTestUser)
+	visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+	assert.True(t, visible, "user should have visible activities")
+}
+
+func TestPrivateActivityNoVisibleForOtherUser(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+
+	session := loginUser(t, privateActivityTestOtherUser)
+	visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+	assert.True(t, visible, "user should have visible activities")
+}
+
+func TestPrivateActivityNoVisibleForAdmin(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+
+	session := loginUser(t, privateActivityTestAdmin)
+	visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+	assert.True(t, visible, "user should have visible activities")
+}
+
+// check activity visibility if the visibility is disabled
+
+func TestPrivateActivityYesInvisibleForPublic(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+	testPrivateActivityHelperEnablePrivateActivity(t)
+
+	visible := testPrivateActivityHelperHasVisibleActivitiesFromPublic(t)
+
+	assert.False(t, visible, "user should have no visible activities")
+}
+
+func TestPrivateActivityYesVisibleForUserItself(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+	testPrivateActivityHelperEnablePrivateActivity(t)
+
+	session := loginUser(t, privateActivityTestUser)
+	visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+	assert.True(t, visible, "user should have visible activities")
+}
+
+func TestPrivateActivityYesInvisibleForOtherUser(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+	testPrivateActivityHelperEnablePrivateActivity(t)
+
+	session := loginUser(t, privateActivityTestOtherUser)
+	visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+	assert.False(t, visible, "user should have no visible activities")
+}
+
+func TestPrivateActivityYesVisibleForAdmin(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+	testPrivateActivityHelperEnablePrivateActivity(t)
+
+	session := loginUser(t, privateActivityTestAdmin)
+	visible := testPrivateActivityHelperHasVisibleActivitiesFromSession(t, session)
+
+	assert.True(t, visible, "user should have visible activities")
+}
+
+// check heatmap visibility if the visibility is enabled
+
+func TestPrivateActivityNoHeatmapVisibleForPublic(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+
+	visible := testPrivateActivityHelperHasVisibleHeatmapFromPublic(t)
+
+	assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityNoHeatmapVisibleForUserItselfAtProfile(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+
+	session := loginUser(t, privateActivityTestUser)
+	visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+	assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityNoHeatmapVisibleForUserItselfAtDashboard(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+
+	session := loginUser(t, privateActivityTestUser)
+	visible := testPrivateActivityHelperHasVisibleDashboardHeatmapFromSession(t, session)
+
+	assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityNoHeatmapVisibleForOtherUser(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+
+	session := loginUser(t, privateActivityTestOtherUser)
+	visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+	assert.True(t, visible, "user should have visible heatmap")
+}
+
+func TestPrivateActivityNoHeatmapVisibleForAdmin(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+
+	session := loginUser(t, privateActivityTestAdmin)
+	visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+	assert.True(t, visible, "user should have visible heatmap")
+}
+
+// check heatmap visibility if the visibility is disabled
+// this behavior, in special the one for the admin, is
+// due to the fact that the heatmap is the same for all viewers;
+// otherwise, there is no reason for it
+
+func TestPrivateActivityYesHeatmapInvisibleForPublic(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+	testPrivateActivityHelperEnablePrivateActivity(t)
+
+	visible := testPrivateActivityHelperHasVisibleHeatmapFromPublic(t)
+
+	assert.False(t, visible, "user should have no visible heatmap")
+}
+
+func TestPrivateActivityYesHeatmapInvisibleForUserItselfAtProfile(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+	testPrivateActivityHelperEnablePrivateActivity(t)
+
+	session := loginUser(t, privateActivityTestUser)
+	visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+	assert.False(t, visible, "user should have no visible heatmap")
+}
+
+func TestPrivateActivityYesHeatmapInvisibleForUserItselfAtDashboard(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+	testPrivateActivityHelperEnablePrivateActivity(t)
+
+	session := loginUser(t, privateActivityTestUser)
+	visible := testPrivateActivityHelperHasVisibleDashboardHeatmapFromSession(t, session)
+
+	assert.False(t, visible, "user should have no visible heatmap")
+}
+
+func TestPrivateActivityYesHeatmapInvisibleForOtherUser(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+	testPrivateActivityHelperEnablePrivateActivity(t)
+
+	session := loginUser(t, privateActivityTestOtherUser)
+	visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+	assert.False(t, visible, "user should have no visible heatmap")
+}
+
+func TestPrivateActivityYesHeatmapInvsisibleForAdmin(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+	testPrivateActivityHelperEnablePrivateActivity(t)
+
+	session := loginUser(t, privateActivityTestAdmin)
+	visible := testPrivateActivityHelperHasVisibleProfileHeatmapFromSession(t, session)
+
+	assert.False(t, visible, "user should have no visible heatmap")
+}
+
+// check heatmap api provides content if the visibility is enabled
+
+func TestPrivateActivityNoHeatmapHasContentForPublic(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+
+	hasContent := testPrivateActivityHelperHasHeatmapContentFromPublic(t)
+
+	assert.True(t, hasContent, "user should have heatmap content")
+}
+
+func TestPrivateActivityNoHeatmapHasContentForUserItself(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+
+	session := loginUser(t, privateActivityTestUser)
+	hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+	assert.True(t, hasContent, "user should have heatmap content")
+}
+
+func TestPrivateActivityNoHeatmapHasContentForOtherUser(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+
+	session := loginUser(t, privateActivityTestOtherUser)
+	hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+	assert.True(t, hasContent, "user should have heatmap content")
+}
+
+func TestPrivateActivityNoHeatmapHasContentForAdmin(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+
+	session := loginUser(t, privateActivityTestAdmin)
+	hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+	assert.True(t, hasContent, "user should have heatmap content")
+}
+
+// check heatmap api provides no content if the visibility is disabled
+// this should be equal to the hidden heatmap at the UI
+
+func TestPrivateActivityYesHeatmapHasNoContentForPublic(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+	testPrivateActivityHelperEnablePrivateActivity(t)
+
+	hasContent := testPrivateActivityHelperHasHeatmapContentFromPublic(t)
+
+	assert.False(t, hasContent, "user should have no heatmap content")
+}
+
+func TestPrivateActivityYesHeatmapHasNoContentForUserItself(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+	testPrivateActivityHelperEnablePrivateActivity(t)
+
+	session := loginUser(t, privateActivityTestUser)
+	hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+	assert.False(t, hasContent, "user should have no heatmap content")
+}
+
+func TestPrivateActivityYesHeatmapHasNoContentForOtherUser(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+	testPrivateActivityHelperEnablePrivateActivity(t)
+
+	session := loginUser(t, privateActivityTestOtherUser)
+	hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+	assert.False(t, hasContent, "user should have no heatmap content")
+}
+
+func TestPrivateActivityYesHeatmapHasNoContentForAdmin(t *testing.T) {
+	defer prepareTestEnv(t)()
+	testPrivateActivityDoSomethingForActionEntries(t)
+	testPrivateActivityHelperEnablePrivateActivity(t)
+
+	session := loginUser(t, privateActivityTestAdmin)
+	hasContent := testPrivateActivityHelperHasHeatmapContentFromSession(t, session)
+
+	assert.False(t, hasContent, "user should have no heatmap content")
+}
diff --git a/models/action.go b/models/action.go
index fd49c6d4ed..59ccdb2d4c 100644
--- a/models/action.go
+++ b/models/action.go
@@ -319,6 +319,12 @@ func GetFeeds(opts GetFeedsOptions) ([]*Action, error) {
 		cond = cond.And(builder.In("repo_id", AccessibleRepoIDsQuery(opts.Actor)))
 	}
 
+	if opts.Actor == nil || !opts.Actor.IsAdmin {
+		if opts.RequestedUser.KeepActivityPrivate && actorID != opts.RequestedUser.ID {
+			return make([]*Action, 0), nil
+		}
+	}
+
 	cond = cond.And(builder.Eq{"user_id": opts.RequestedUser.ID})
 
 	if opts.OnlyPerformedBy {
diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go
index 869661aee4..432bcffb1b 100644
--- a/models/migrations/migrations.go
+++ b/models/migrations/migrations.go
@@ -214,6 +214,8 @@ var migrations = []Migration{
 	NewMigration("prepend refs/heads/ to issue refs", prependRefsHeadsToIssueRefs),
 	// v140 -> v141
 	NewMigration("Save detected language file size to database instead of percent", fixLanguageStatsToSaveSize),
+	// v141 -> 142
+	NewMigration("Add KeepActivityPrivate to User table", addKeepActivityPrivateUserColumn),
 }
 
 // GetCurrentDBVersion returns the current db version
diff --git a/models/migrations/v141.go b/models/migrations/v141.go
new file mode 100644
index 0000000000..b5824ecd48
--- /dev/null
+++ b/models/migrations/v141.go
@@ -0,0 +1,22 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package migrations
+
+import (
+	"fmt"
+
+	"xorm.io/xorm"
+)
+
+func addKeepActivityPrivateUserColumn(x *xorm.Engine) error {
+	type User struct {
+		KeepActivityPrivate bool
+	}
+
+	if err := x.Sync2(new(User)); err != nil {
+		return fmt.Errorf("Sync2: %v", err)
+	}
+	return nil
+}
diff --git a/models/user.go b/models/user.go
index 8875840db7..0ecb1b9a48 100644
--- a/models/user.go
+++ b/models/user.go
@@ -163,8 +163,9 @@ type User struct {
 	RepoAdminChangeTeamAccess bool                `xorm:"NOT NULL DEFAULT false"`
 
 	// Preferences
-	DiffViewStyle string `xorm:"NOT NULL DEFAULT ''"`
-	Theme         string `xorm:"NOT NULL DEFAULT ''"`
+	DiffViewStyle       string `xorm:"NOT NULL DEFAULT ''"`
+	Theme               string `xorm:"NOT NULL DEFAULT ''"`
+	KeepActivityPrivate bool   `xorm:"NOT NULL DEFAULT false"`
 }
 
 // SearchOrganizationsOptions options to filter organizations
diff --git a/models/user_heatmap.go b/models/user_heatmap.go
index 3d9e0683fc..ce3ec029ca 100644
--- a/models/user_heatmap.go
+++ b/models/user_heatmap.go
@@ -18,6 +18,11 @@ type UserHeatmapData struct {
 // GetUserHeatmapDataByUser returns an array of UserHeatmapData
 func GetUserHeatmapDataByUser(user *User) ([]*UserHeatmapData, error) {
 	hdata := make([]*UserHeatmapData, 0)
+
+	if user.KeepActivityPrivate {
+		return hdata, nil
+	}
+
 	var groupBy string
 	var groupByName = "timestamp" // We need this extra case because mssql doesn't allow grouping by alias
 	switch {
diff --git a/modules/auth/user_form.go b/modules/auth/user_form.go
index 0c191fbc07..999d4cd74d 100644
--- a/modules/auth/user_form.go
+++ b/modules/auth/user_form.go
@@ -196,14 +196,15 @@ func (f *AccessTokenForm) Validate(ctx *macaron.Context, errs binding.Errors) bi
 
 // UpdateProfileForm form for updating profile
 type UpdateProfileForm struct {
-	Name             string `binding:"AlphaDashDot;MaxSize(40)"`
-	FullName         string `binding:"MaxSize(100)"`
-	Email            string `binding:"Required;Email;MaxSize(254)"`
-	KeepEmailPrivate bool
-	Website          string `binding:"ValidUrl;MaxSize(255)"`
-	Location         string `binding:"MaxSize(50)"`
-	Language         string `binding:"Size(5)"`
-	Description      string `binding:"MaxSize(255)"`
+	Name                string `binding:"AlphaDashDot;MaxSize(40)"`
+	FullName            string `binding:"MaxSize(100)"`
+	Email               string `binding:"Required;Email;MaxSize(254)"`
+	KeepEmailPrivate    bool
+	Website             string `binding:"ValidUrl;MaxSize(255)"`
+	Location            string `binding:"MaxSize(50)"`
+	Language            string `binding:"Size(5)"`
+	Description         string `binding:"MaxSize(255)"`
+	KeepActivityPrivate bool
 }
 
 // Validate validates the fields
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index f2e58b95b8..6227ceb2a2 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -392,6 +392,7 @@ follow = Follow
 unfollow = Unfollow
 heatmap.loading = Loading Heatmap…
 user_bio = Biography
+disabled_public_activity = This user has disabled the public visibility of the activity.
 
 form.name_reserved = The username '%s' is reserved.
 form.name_pattern_not_allowed = The pattern '%s' is not allowed in a username.
@@ -430,6 +431,9 @@ continue = Continue
 cancel = Cancel
 language = Language
 ui = Theme
+privacy = Privacy
+keep_activity_private = Hide the activity from the profile page
+keep_activity_private_popup = Makes the activity visible only for you and the admins
 
 lookup_avatar_by_mail = Look Up Avatar by Email Address
 federated_avatar_lookup = Federated Avatar Lookup
diff --git a/routers/user/home.go b/routers/user/home.go
index 2fc0c60aad..4e5fc3e4df 100644
--- a/routers/user/home.go
+++ b/routers/user/home.go
@@ -112,7 +112,9 @@ func Dashboard(ctx *context.Context) {
 	ctx.Data["PageIsDashboard"] = true
 	ctx.Data["PageIsNews"] = true
 	ctx.Data["SearchLimit"] = setting.UI.User.RepoPagingNum
-	ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap
+	// no heatmap access for admins; GetUserHeatmapDataByUser ignores the calling user
+	// so everyone would get the same empty heatmap
+	ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap && !ctxUser.KeepActivityPrivate
 	ctx.Data["HeatmapUser"] = ctxUser.Name
 
 	var err error
diff --git a/routers/user/profile.go b/routers/user/profile.go
index 215dff0084..82fab4ad87 100644
--- a/routers/user/profile.go
+++ b/routers/user/profile.go
@@ -93,7 +93,9 @@ func Profile(ctx *context.Context) {
 	ctx.Data["PageIsUserProfile"] = true
 	ctx.Data["Owner"] = ctxUser
 	ctx.Data["OpenIDs"] = openIDs
-	ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap
+	// no heatmap access for admins; GetUserHeatmapDataByUser ignores the calling user
+	// so everyone would get the same empty heatmap
+	ctx.Data["EnableHeatmap"] = setting.Service.EnableUserHeatmap && !ctxUser.KeepActivityPrivate
 	ctx.Data["HeatmapUser"] = ctxUser.Name
 	showPrivate := ctx.IsSigned && (ctx.User.IsAdmin || ctx.User.ID == ctxUser.ID)
 
diff --git a/routers/user/setting/profile.go b/routers/user/setting/profile.go
index d6f25f9135..ba9ba2b257 100644
--- a/routers/user/setting/profile.go
+++ b/routers/user/setting/profile.go
@@ -96,6 +96,7 @@ func ProfilePost(ctx *context.Context, form auth.UpdateProfileForm) {
 	ctx.User.Location = form.Location
 	ctx.User.Language = form.Language
 	ctx.User.Description = form.Description
+	ctx.User.KeepActivityPrivate = form.KeepActivityPrivate
 	if err := models.UpdateUserSetting(ctx.User); err != nil {
 		if _, ok := err.(models.ErrEmailAlreadyUsed); ok {
 			ctx.Flash.Error(ctx.Tr("form.email_been_used"))
diff --git a/templates/user/profile.tmpl b/templates/user/profile.tmpl
index e07b4b0dd8..563bc78307 100644
--- a/templates/user/profile.tmpl
+++ b/templates/user/profile.tmpl
@@ -104,10 +104,15 @@
 				</div>
 
 				{{if eq .TabName "activity"}}
-				{{if .EnableHeatmap}}
-					{{template "user/dashboard/heatmap" .}}
-					<div class="ui divider"></div>
-				{{end}}
+					{{if .Owner.KeepActivityPrivate}}
+						<div class="ui info message">
+							<p>{{.i18n.Tr "user.disabled_public_activity"}}</p>
+						</div>
+					{{end}}
+					{{if .EnableHeatmap}}
+						{{template "user/dashboard/heatmap" .}}
+						<div class="ui divider"></div>
+					{{end}}
 					<div class="feeds">
 						{{template "user/dashboard/feeds" .}}
 					</div>
diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl
index 995bdfd638..b170c67579 100644
--- a/templates/user/settings/profile.tmpl
+++ b/templates/user/settings/profile.tmpl
@@ -58,6 +58,13 @@
 						</div>
 					</div>
 
+				<div class="field">
+					<label for="keep-activity-private">{{.i18n.Tr "settings.privacy"}}</label>
+					<div class="ui checkbox" id="keep-activity-private">
+						<label class="poping up" data-content="{{.i18n.Tr "settings.keep_activity_private_popup"}}"><strong>{{.i18n.Tr "settings.keep_activity_private"}}</strong></label>
+						<input name="keep_activity_private" type="checkbox" {{if .SignedUser.KeepActivityPrivate}}checked{{end}}>
+					</div>
+				</div>
 				<div class="field">
 					<button class="ui green button">{{$.i18n.Tr "settings.update_profile"}}</button>
 				</div>