diff --git a/models/issues/issue.go b/models/issues/issue.go
index 5bdb60f7c0..49bc229c6b 100644
--- a/models/issues/issue.go
+++ b/models/issues/issue.go
@@ -1186,6 +1186,7 @@ type IssuesOptions struct { //nolint
 	PosterID           int64
 	MentionedID        int64
 	ReviewRequestedID  int64
+	SubscriberID       int64
 	MilestoneIDs       []int64
 	ProjectID          int64
 	ProjectBoardID     int64
@@ -1299,6 +1300,10 @@ func (opts *IssuesOptions) setupSessionNoLimit(sess *xorm.Session) {
 		applyReviewRequestedCondition(sess, opts.ReviewRequestedID)
 	}
 
+	if opts.SubscriberID > 0 {
+		applySubscribedCondition(sess, opts.SubscriberID)
+	}
+
 	if len(opts.MilestoneIDs) > 0 {
 		sess.In("issue.milestone_id", opts.MilestoneIDs)
 	}
@@ -1463,6 +1468,36 @@ func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64)
 			reviewRequestedID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, reviewRequestedID)
 }
 
+func applySubscribedCondition(sess *xorm.Session, subscriberID int64) *xorm.Session {
+	return sess.And(
+		builder.
+			NotIn("issue.id",
+				builder.Select("issue_id").
+					From("issue_watch").
+					Where(builder.Eq{"is_watching": false, "user_id": subscriberID}),
+			),
+	).And(
+		builder.Or(
+			builder.In("issue.id", builder.
+				Select("issue_id").
+				From("issue_watch").
+				Where(builder.Eq{"is_watching": true, "user_id": subscriberID}),
+			),
+			builder.In("issue.id", builder.
+				Select("issue_id").
+				From("comment").
+				Where(builder.Eq{"poster_id": subscriberID}),
+			),
+			builder.Eq{"issue.poster_id": subscriberID},
+			builder.In("issue.repo_id", builder.
+				Select("id").
+				From("watch").
+				Where(builder.Eq{"user_id": subscriberID, "mode": true}),
+			),
+		),
+	)
+}
+
 // CountIssuesByRepo map from repoID to number of issues matching the options
 func CountIssuesByRepo(opts *IssuesOptions) (map[int64]int64, error) {
 	e := db.GetEngine(db.DefaultContext)
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index 991ebf344f..1dba1d71d8 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -3034,6 +3034,9 @@ pin = Pin notification
 mark_as_read = Mark as read
 mark_as_unread = Mark as unread
 mark_all_as_read = Mark all as read
+subscriptions = Subscriptions
+watching = Watching
+no_subscriptions = No subscriptions
 
 [gpg]
 default_key=Signed with default key
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go
index 5e8142cec7..b4753a603e 100644
--- a/routers/web/user/notification.go
+++ b/routers/web/user/notification.go
@@ -13,16 +13,23 @@ import (
 	"strings"
 
 	activities_model "code.gitea.io/gitea/models/activities"
+	"code.gitea.io/gitea/models/db"
+	issues_model "code.gitea.io/gitea/models/issues"
+	repo_model "code.gitea.io/gitea/models/repo"
 	"code.gitea.io/gitea/modules/base"
 	"code.gitea.io/gitea/modules/context"
 	"code.gitea.io/gitea/modules/log"
 	"code.gitea.io/gitea/modules/setting"
 	"code.gitea.io/gitea/modules/structs"
+	"code.gitea.io/gitea/modules/util"
+	issue_service "code.gitea.io/gitea/services/issue"
+	pull_service "code.gitea.io/gitea/services/pull"
 )
 
 const (
-	tplNotification    base.TplName = "user/notification/notification"
-	tplNotificationDiv base.TplName = "user/notification/notification_div"
+	tplNotification              base.TplName = "user/notification/notification"
+	tplNotificationDiv           base.TplName = "user/notification/notification_div"
+	tplNotificationSubscriptions base.TplName = "user/notification/notification_subscriptions"
 )
 
 // GetNotificationCount is the middleware that sets the notification count in the context
@@ -197,6 +204,208 @@ func NotificationPurgePost(c *context.Context) {
 	c.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther)
 }
 
+// NotificationSubscriptions returns the list of subscribed issues
+func NotificationSubscriptions(c *context.Context) {
+	page := c.FormInt("page")
+	if page < 1 {
+		page = 1
+	}
+
+	sortType := c.FormString("sort")
+	c.Data["SortType"] = sortType
+
+	state := c.FormString("state")
+	if !util.IsStringInSlice(state, []string{"all", "open", "closed"}, true) {
+		state = "all"
+	}
+	c.Data["State"] = state
+	var showClosed util.OptionalBool
+	switch state {
+	case "all":
+		showClosed = util.OptionalBoolNone
+	case "closed":
+		showClosed = util.OptionalBoolTrue
+	case "open":
+		showClosed = util.OptionalBoolFalse
+	}
+
+	var issueTypeBool util.OptionalBool
+	issueType := c.FormString("issueType")
+	switch issueType {
+	case "issues":
+		issueTypeBool = util.OptionalBoolFalse
+	case "pulls":
+		issueTypeBool = util.OptionalBoolTrue
+	default:
+		issueTypeBool = util.OptionalBoolNone
+	}
+	c.Data["IssueType"] = issueType
+
+	var labelIDs []int64
+	selectedLabels := c.FormString("labels")
+	c.Data["Labels"] = selectedLabels
+	if len(selectedLabels) > 0 && selectedLabels != "0" {
+		var err error
+		labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
+		if err != nil {
+			c.ServerError("StringsToInt64s", err)
+			return
+		}
+	}
+
+	count, err := issues_model.CountIssues(&issues_model.IssuesOptions{
+		SubscriberID: c.Doer.ID,
+		IsClosed:     showClosed,
+		IsPull:       issueTypeBool,
+		LabelIDs:     labelIDs,
+	})
+	if err != nil {
+		c.ServerError("CountIssues", err)
+		return
+	}
+	issues, err := issues_model.Issues(&issues_model.IssuesOptions{
+		ListOptions: db.ListOptions{
+			PageSize: setting.UI.IssuePagingNum,
+			Page:     page,
+		},
+		SubscriberID: c.Doer.ID,
+		SortType:     sortType,
+		IsClosed:     showClosed,
+		IsPull:       issueTypeBool,
+		LabelIDs:     labelIDs,
+	})
+	if err != nil {
+		c.ServerError("Issues", err)
+		return
+	}
+
+	commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(c, issues)
+	if err != nil {
+		c.ServerError("GetIssuesAllCommitStatus", err)
+		return
+	}
+	c.Data["CommitLastStatus"] = lastStatus
+	c.Data["CommitStatuses"] = commitStatuses
+	c.Data["Issues"] = issues
+
+	c.Data["IssueRefEndNames"], c.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, "")
+
+	commitStatus, err := pull_service.GetIssuesLastCommitStatus(c, issues)
+	if err != nil {
+		c.ServerError("GetIssuesLastCommitStatus", err)
+		return
+	}
+	c.Data["CommitStatus"] = commitStatus
+
+	issueList := issues_model.IssueList(issues)
+	approvalCounts, err := issueList.GetApprovalCounts(c)
+	if err != nil {
+		c.ServerError("ApprovalCounts", err)
+		return
+	}
+	c.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
+		counts, ok := approvalCounts[issueID]
+		if !ok || len(counts) == 0 {
+			return 0
+		}
+		reviewTyp := issues_model.ReviewTypeApprove
+		if typ == "reject" {
+			reviewTyp = issues_model.ReviewTypeReject
+		} else if typ == "waiting" {
+			reviewTyp = issues_model.ReviewTypeRequest
+		}
+		for _, count := range counts {
+			if count.Type == reviewTyp {
+				return count.Count
+			}
+		}
+		return 0
+	}
+
+	c.Data["Status"] = 1
+	c.Data["Title"] = c.Tr("notification.subscriptions")
+
+	// redirect to last page if request page is more than total pages
+	pager := context.NewPagination(int(count), setting.UI.IssuePagingNum, page, 5)
+	if pager.Paginater.Current() < page {
+		c.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current()))
+		return
+	}
+	pager.AddParam(c, "sort", "SortType")
+	pager.AddParam(c, "state", "State")
+	c.Data["Page"] = pager
+
+	c.HTML(http.StatusOK, tplNotificationSubscriptions)
+}
+
+// NotificationWatching returns the list of watching repos
+func NotificationWatching(c *context.Context) {
+	page := c.FormInt("page")
+	if page < 1 {
+		page = 1
+	}
+
+	var orderBy db.SearchOrderBy
+	c.Data["SortType"] = c.FormString("sort")
+	switch c.FormString("sort") {
+	case "newest":
+		orderBy = db.SearchOrderByNewest
+	case "oldest":
+		orderBy = db.SearchOrderByOldest
+	case "recentupdate":
+		orderBy = db.SearchOrderByRecentUpdated
+	case "leastupdate":
+		orderBy = db.SearchOrderByLeastUpdated
+	case "reversealphabetically":
+		orderBy = db.SearchOrderByAlphabeticallyReverse
+	case "alphabetically":
+		orderBy = db.SearchOrderByAlphabetically
+	case "moststars":
+		orderBy = db.SearchOrderByStarsReverse
+	case "feweststars":
+		orderBy = db.SearchOrderByStars
+	case "mostforks":
+		orderBy = db.SearchOrderByForksReverse
+	case "fewestforks":
+		orderBy = db.SearchOrderByForks
+	default:
+		c.Data["SortType"] = "recentupdate"
+		orderBy = db.SearchOrderByRecentUpdated
+	}
+
+	repos, count, err := repo_model.SearchRepository(&repo_model.SearchRepoOptions{
+		ListOptions: db.ListOptions{
+			PageSize: setting.UI.User.RepoPagingNum,
+			Page:     page,
+		},
+		Actor:              c.Doer,
+		Keyword:            c.FormTrim("q"),
+		OrderBy:            orderBy,
+		Private:            c.IsSigned,
+		WatchedByID:        c.Doer.ID,
+		Collaborate:        util.OptionalBoolFalse,
+		TopicOnly:          c.FormBool("topic"),
+		IncludeDescription: setting.UI.SearchRepoDescription,
+	})
+	if err != nil {
+		c.ServerError("ErrSearchRepository", err)
+		return
+	}
+	total := int(count)
+	c.Data["Total"] = total
+	c.Data["Repos"] = repos
+
+	// redirect to last page if request page is more than total pages
+	pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5)
+	pager.SetDefaultParams(c)
+	c.Data["Page"] = pager
+
+	c.Data["Status"] = 2
+	c.Data["Title"] = c.Tr("notification.watching")
+
+	c.HTML(http.StatusOK, tplNotificationSubscriptions)
+}
+
 // NewAvailable returns the notification counts
 func NewAvailable(ctx *context.Context) {
 	ctx.JSON(http.StatusOK, structs.NotificationCount{New: activities_model.CountUnread(ctx, ctx.Doer.ID)})
diff --git a/routers/web/web.go b/routers/web/web.go
index 1852ecc2e2..acce071891 100644
--- a/routers/web/web.go
+++ b/routers/web/web.go
@@ -1269,6 +1269,8 @@ func RegisterRoutes(m *web.Route) {
 
 	m.Group("/notifications", func() {
 		m.Get("", user.Notifications)
+		m.Get("/subscriptions", user.NotificationSubscriptions)
+		m.Get("/watching", user.NotificationWatching)
 		m.Post("/status", user.NotificationStatusPost)
 		m.Post("/purge", user.NotificationPurgePost)
 		m.Get("/new", user.NewAvailable)
diff --git a/templates/base/head_navbar.tmpl b/templates/base/head_navbar.tmpl
index 8cd3b0a4ae..12837ebefe 100644
--- a/templates/base/head_navbar.tmpl
+++ b/templates/base/head_navbar.tmpl
@@ -171,6 +171,10 @@
 							{{.locale.Tr "your_starred"}}
 						</a>
 					{{end}}
+					<a class="item" href="{{AppSubUrl}}/notifications/subscriptions">
+						{{svg "octicon-bell"}}
+						{{.locale.Tr "notification.subscriptions"}}<!-- Subscriptions -->
+					</a>
 					<a class="{{if .PageIsUserSettings}}active{{end}} item" href="{{AppSubUrl}}/user/settings">
 						{{svg "octicon-tools"}}
 						{{.locale.Tr "your_settings"}}<!-- Your settings -->
diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl
new file mode 100644
index 0000000000..aa89c12dde
--- /dev/null
+++ b/templates/user/notification/notification_subscriptions.tmpl
@@ -0,0 +1,79 @@
+{{template "base/head" .}}
+<div class="page-content user notification" id="notification_subscriptions" data-params="{{.Page.GetParams}}" data-sequence-number="{{.SequenceNumber}}">
+	<div class="ui container">
+		<div class="ui top attached tabular menu">
+			<a href="{{AppSubUrl}}/notifications/subscriptions" class="{{if eq .Status 1}}active {{end}}item">
+				{{.locale.Tr "notification.subscriptions"}}
+			</a>
+			<a href="{{AppSubUrl}}/notifications/watching" class="{{if eq .Status 2}}active {{end}}item">
+				{{.locale.Tr "notification.watching"}}
+			</a>
+		</div>
+		<div class="ui bottom attached active tab segment">
+			{{if eq .Status 1}}
+				<div id="issue-filters" class="ui stackable grid">
+					<div class="six wide column">
+						<div class="ui compact tiny menu">
+							<a class="{{if eq .State "all"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=all&issueType={{$.IssueType}}&labels={{$.Labels}}">
+								{{.locale.Tr "all"}}
+							</a>
+							<a class="{{if eq .State "open"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=open&issueType={{$.IssueType}}&labels={{$.Labels}}">
+								{{svg "octicon-issue-opened" 16 "mr-3"}}
+								{{.locale.Tr "repo.issues.open_title"}}
+							</a>
+							<a class="{{if eq .State "closed"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=closed&issueType={{$.IssueType}}&labels={{$.Labels}}">
+								{{svg "octicon-issue-closed" 16 "mr-3"}}
+								{{.locale.Tr "repo.issues.closed_title"}}
+							</a>
+						</div>
+					</div>
+					<div class="seven wide right aligned right floated column">
+						<div class="ui right aligned secondary filter stackable menu labels">
+							<!-- Type -->
+								<div class="ui dropdown type jump item">
+									<span class="text">
+										{{.locale.Tr "repo.issues.filter_type"}}
+										{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+									</span>
+									<div class="menu">
+										<a class="{{if or (eq .IssueType "all") (not .IssueType)}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=all&labels={{$.Labels}}">{{.locale.Tr "all"}}</a>
+										<a class="{{if eq .IssueType "issues"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=issues&labels={{$.Labels}}">{{.locale.Tr "issues"}}</a>
+										<a class="{{if eq .IssueType "pulls"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=pulls&labels={{$.Labels}}">{{.locale.Tr "pull_requests"}}</a>
+									</div>
+								</div>
+
+							<!-- Sort -->
+							<div class="ui dropdown type jump item">
+								<span class="text">
+									{{.locale.Tr "repo.issues.filter_sort"}}
+									{{svg "octicon-triangle-down" 14 "dropdown icon"}}
+								</span>
+								<div class="menu">
+									<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=latest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.latest"}}</a>
+									<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?sort=oldest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.oldest"}}</a>
+									<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.recentupdate"}}</a>
+									<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.leastupdate"}}</a>
+									<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{$.Link}}?sort=mostcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.mostcomment"}}</a>
+									<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{$.Link}}?sort=leastcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.leastcomment"}}</a>
+									<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{$.Link}}?sort=nearduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.nearduedate"}}</a>
+									<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{$.Link}}?sort=farduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.farduedate"}}</a>
+								</div>
+							</div>
+						</div>
+					</div>
+				</div>
+				{{if eq (len .Issues) 0}}
+					<div class="ui divider"></div>
+					{{.locale.Tr "notification.no_subscriptions"}}
+				{{else}}
+					{{template "shared/issuelist" mergeinto . "listType" "dashboard"}}
+				{{end}}
+			{{else}}
+				{{template "explore/repo_search" .}}
+				{{template "explore/repo_list" .}}
+				{{template "base/paginate" .}}
+			{{end}}
+		</div>
+	</div>
+</div>
+{{template "base/footer" .}}