From 08609d439d81f65c4f691609acaa29c1c7e96757 Mon Sep 17 00:00:00 2001 From: qwerty287 <80460567+qwerty287@users.noreply.github.com> Date: Thu, 29 Sep 2022 21:09:14 +0200 Subject: [PATCH] Add pages to view watched repos and subscribed issues/PRs (#17156) Adds GitHub-like pages to view watched repos and subscribed issues/PRs This is my second try to fix this, but it is better than the first since it doesn't uses a filter option which could be slow when accessing `/issues` or `/pulls` and it shows both pulls and issues (the first try is #17053). Closes #16111 Replaces and closes #17053 ![Screenshot](https://user-images.githubusercontent.com/80460567/134782937-3112f7da-425a-45b6-9511-5c9695aee896.png) Co-authored-by: Lauris BH Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: wxiaoguang --- models/issues/issue.go | 35 +++ options/locale/locale_en-US.ini | 3 + routers/web/user/notification.go | 213 +++++++++++++++++- routers/web/web.go | 2 + templates/base/head_navbar.tmpl | 4 + .../notification_subscriptions.tmpl | 79 +++++++ 6 files changed, 334 insertions(+), 2 deletions(-) create mode 100644 templates/user/notification/notification_subscriptions.tmpl diff --git a/models/issues/issue.go b/models/issues/issue.go index 5bdb60f7c08..49bc229c6bd 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 991ebf344f8..1dba1d71d8f 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 5e8142cec72..b4753a603e8 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 1852ecc2e24..acce0718918 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 8cd3b0a4ae0..12837ebefed 100644 --- a/templates/base/head_navbar.tmpl +++ b/templates/base/head_navbar.tmpl @@ -171,6 +171,10 @@ {{.locale.Tr "your_starred"}} {{end}} + + {{svg "octicon-bell"}} + {{.locale.Tr "notification.subscriptions"}} + {{svg "octicon-tools"}} {{.locale.Tr "your_settings"}} diff --git a/templates/user/notification/notification_subscriptions.tmpl b/templates/user/notification/notification_subscriptions.tmpl new file mode 100644 index 00000000000..aa89c12dded --- /dev/null +++ b/templates/user/notification/notification_subscriptions.tmpl @@ -0,0 +1,79 @@ +{{template "base/head" .}} +
+
+ +
+ {{if eq .Status 1}} + + {{if eq (len .Issues) 0}} +
+ {{.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}} +
+
+
+{{template "base/footer" .}}