aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--models/issues/issue.go35
-rw-r--r--options/locale/locale_en-US.ini3
-rw-r--r--routers/web/user/notification.go213
-rw-r--r--routers/web/web.go2
-rw-r--r--templates/base/head_navbar.tmpl4
-rw-r--r--templates/user/notification/notification_subscriptions.tmpl79
6 files changed, 334 insertions, 2 deletions
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" .}}