diff options
Diffstat (limited to '')
-rw-r--r-- | routers/web/user/notification.go | 485 |
1 files changed, 485 insertions, 0 deletions
diff --git a/routers/web/user/notification.go b/routers/web/user/notification.go new file mode 100644 index 0000000..dfcaf58 --- /dev/null +++ b/routers/web/user/notification.go @@ -0,0 +1,485 @@ +// Copyright 2019 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package user + +import ( + goctx "context" + "errors" + "fmt" + "net/http" + "net/url" + "strings" + + activities_model "code.gitea.io/gitea/models/activities" + "code.gitea.io/gitea/models/db" + git_model "code.gitea.io/gitea/models/git" + issues_model "code.gitea.io/gitea/models/issues" + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/models/unit" + "code.gitea.io/gitea/modules/base" + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/optional" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/structs" + "code.gitea.io/gitea/modules/util" + "code.gitea.io/gitea/services/context" + 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" + tplNotificationSubscriptions base.TplName = "user/notification/notification_subscriptions" +) + +// GetNotificationCount is the middleware that sets the notification count in the context +func GetNotificationCount(ctx *context.Context) { + if strings.HasPrefix(ctx.Req.URL.Path, "/api") { + return + } + + if !ctx.IsSigned { + return + } + + ctx.Data["NotificationUnreadCount"] = func() int64 { + count, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{ + UserID: ctx.Doer.ID, + Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread}, + }) + if err != nil { + if err != goctx.Canceled { + log.Error("Unable to GetNotificationCount for user:%-v: %v", ctx.Doer, err) + } + return -1 + } + + return count + } +} + +// Notifications is the notifications page +func Notifications(ctx *context.Context) { + getNotifications(ctx) + if ctx.Written() { + return + } + if ctx.FormBool("div-only") { + ctx.Data["SequenceNumber"] = ctx.FormString("sequence-number") + ctx.HTML(http.StatusOK, tplNotificationDiv) + return + } + ctx.HTML(http.StatusOK, tplNotification) +} + +func getNotifications(ctx *context.Context) { + var ( + keyword = ctx.FormTrim("q") + status activities_model.NotificationStatus + page = ctx.FormInt("page") + perPage = ctx.FormInt("perPage") + ) + if page < 1 { + page = 1 + } + if perPage < 1 { + perPage = 20 + } + + switch keyword { + case "read": + status = activities_model.NotificationStatusRead + default: + status = activities_model.NotificationStatusUnread + } + + total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{ + UserID: ctx.Doer.ID, + Status: []activities_model.NotificationStatus{status}, + }) + if err != nil { + ctx.ServerError("ErrGetNotificationCount", err) + return + } + + // redirect to last page if request page is more than total pages + pager := context.NewPagination(int(total), perPage, page, 5) + if pager.Paginater.Current() < page { + ctx.Redirect(fmt.Sprintf("%s/notifications?q=%s&page=%d", setting.AppSubURL, url.QueryEscape(ctx.FormString("q")), pager.Paginater.Current())) + return + } + + statuses := []activities_model.NotificationStatus{status, activities_model.NotificationStatusPinned} + nls, err := db.Find[activities_model.Notification](ctx, activities_model.FindNotificationOptions{ + ListOptions: db.ListOptions{ + PageSize: perPage, + Page: page, + }, + UserID: ctx.Doer.ID, + Status: statuses, + }) + if err != nil { + ctx.ServerError("db.Find[activities_model.Notification]", err) + return + } + + notifications := activities_model.NotificationList(nls) + + failCount := 0 + + repos, failures, err := notifications.LoadRepos(ctx) + if err != nil { + ctx.ServerError("LoadRepos", err) + return + } + notifications = notifications.Without(failures) + if err := repos.LoadAttributes(ctx); err != nil { + ctx.ServerError("LoadAttributes", err) + return + } + failCount += len(failures) + + failures, err = notifications.LoadIssues(ctx) + if err != nil { + ctx.ServerError("LoadIssues", err) + return + } + + if err = notifications.LoadIssuePullRequests(ctx); err != nil { + ctx.ServerError("LoadIssuePullRequests", err) + return + } + + notifications = notifications.Without(failures) + failCount += len(failures) + + failures, err = notifications.LoadComments(ctx) + if err != nil { + ctx.ServerError("LoadComments", err) + return + } + notifications = notifications.Without(failures) + failCount += len(failures) + + if failCount > 0 { + ctx.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount)) + } + + ctx.Data["Title"] = ctx.Tr("notifications") + ctx.Data["Keyword"] = keyword + ctx.Data["Status"] = status + ctx.Data["Notifications"] = notifications + + pager.SetDefaultParams(ctx) + ctx.Data["Page"] = pager +} + +// NotificationStatusPost is a route for changing the status of a notification +func NotificationStatusPost(ctx *context.Context) { + var ( + notificationID = ctx.FormInt64("notification_id") + statusStr = ctx.FormString("status") + status activities_model.NotificationStatus + ) + + switch statusStr { + case "read": + status = activities_model.NotificationStatusRead + case "unread": + status = activities_model.NotificationStatusUnread + case "pinned": + status = activities_model.NotificationStatusPinned + default: + ctx.ServerError("InvalidNotificationStatus", errors.New("Invalid notification status")) + return + } + + if _, err := activities_model.SetNotificationStatus(ctx, notificationID, ctx.Doer, status); err != nil { + ctx.ServerError("SetNotificationStatus", err) + return + } + + if !ctx.FormBool("noredirect") { + url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, url.QueryEscape(ctx.FormString("page"))) + ctx.Redirect(url, http.StatusSeeOther) + } + + getNotifications(ctx) + if ctx.Written() { + return + } + ctx.Data["Link"] = setting.AppSubURL + "/notifications" + ctx.Data["SequenceNumber"] = ctx.Req.PostFormValue("sequence-number") + + ctx.HTML(http.StatusOK, tplNotificationDiv) +} + +// NotificationPurgePost is a route for 'purging' the list of notifications - marking all unread as read +func NotificationPurgePost(ctx *context.Context) { + err := activities_model.UpdateNotificationStatuses(ctx, ctx.Doer, activities_model.NotificationStatusUnread, activities_model.NotificationStatusRead) + if err != nil { + ctx.ServerError("UpdateNotificationStatuses", err) + return + } + + ctx.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther) +} + +// NotificationSubscriptions returns the list of subscribed issues +func NotificationSubscriptions(ctx *context.Context) { + page := ctx.FormInt("page") + if page < 1 { + page = 1 + } + + sortType := ctx.FormString("sort") + ctx.Data["SortType"] = sortType + + state := ctx.FormString("state") + if !util.SliceContainsString([]string{"all", "open", "closed"}, state, true) { + state = "all" + } + + ctx.Data["State"] = state + // default state filter is "all" + showClosed := optional.None[bool]() + switch state { + case "closed": + showClosed = optional.Some(true) + case "open": + showClosed = optional.Some(false) + } + + issueType := ctx.FormString("issueType") + // default issue type is no filter + issueTypeBool := optional.None[bool]() + switch issueType { + case "issues": + issueTypeBool = optional.Some(false) + case "pulls": + issueTypeBool = optional.Some(true) + } + ctx.Data["IssueType"] = issueType + + var labelIDs []int64 + selectedLabels := ctx.FormString("labels") + ctx.Data["Labels"] = selectedLabels + if len(selectedLabels) > 0 && selectedLabels != "0" { + var err error + labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ",")) + if err != nil { + ctx.Flash.Error(ctx.Tr("invalid_data", selectedLabels), true) + } + } + + count, err := issues_model.CountIssues(ctx, &issues_model.IssuesOptions{ + SubscriberID: ctx.Doer.ID, + IsClosed: showClosed, + IsPull: issueTypeBool, + LabelIDs: labelIDs, + }) + if err != nil { + ctx.ServerError("CountIssues", err) + return + } + issues, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{ + Paginator: &db.ListOptions{ + PageSize: setting.UI.IssuePagingNum, + Page: page, + }, + SubscriberID: ctx.Doer.ID, + SortType: sortType, + IsClosed: showClosed, + IsPull: issueTypeBool, + LabelIDs: labelIDs, + }) + if err != nil { + ctx.ServerError("Issues", err) + return + } + + commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues) + if err != nil { + ctx.ServerError("GetIssuesAllCommitStatus", err) + return + } + if !ctx.Repo.CanRead(unit.TypeActions) { + for key := range commitStatuses { + git_model.CommitStatusesHideActionsURL(ctx, commitStatuses[key]) + } + } + ctx.Data["CommitLastStatus"] = lastStatus + ctx.Data["CommitStatuses"] = commitStatuses + ctx.Data["Issues"] = issues + + ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, "") + + commitStatus, err := pull_service.GetIssuesLastCommitStatus(ctx, issues) + if err != nil { + ctx.ServerError("GetIssuesLastCommitStatus", err) + return + } + ctx.Data["CommitStatus"] = commitStatus + + approvalCounts, err := issues.GetApprovalCounts(ctx) + if err != nil { + ctx.ServerError("ApprovalCounts", err) + return + } + ctx.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 + } + + ctx.Data["Status"] = 1 + ctx.Data["Title"] = ctx.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 { + ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current())) + return + } + pager.AddParam(ctx, "sort", "SortType") + pager.AddParam(ctx, "state", "State") + ctx.Data["Page"] = pager + + ctx.HTML(http.StatusOK, tplNotificationSubscriptions) +} + +// NotificationWatching returns the list of watching repos +func NotificationWatching(ctx *context.Context) { + page := ctx.FormInt("page") + if page < 1 { + page = 1 + } + + keyword := ctx.FormTrim("q") + ctx.Data["Keyword"] = keyword + + var orderBy db.SearchOrderBy + ctx.Data["SortType"] = ctx.FormString("sort") + switch ctx.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: + ctx.Data["SortType"] = "recentupdate" + orderBy = db.SearchOrderByRecentUpdated + } + + archived := ctx.FormOptionalBool("archived") + ctx.Data["IsArchived"] = archived + + fork := ctx.FormOptionalBool("fork") + ctx.Data["IsFork"] = fork + + mirror := ctx.FormOptionalBool("mirror") + ctx.Data["IsMirror"] = mirror + + template := ctx.FormOptionalBool("template") + ctx.Data["IsTemplate"] = template + + private := ctx.FormOptionalBool("private") + ctx.Data["IsPrivate"] = private + + repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{ + ListOptions: db.ListOptions{ + PageSize: setting.UI.User.RepoPagingNum, + Page: page, + }, + Actor: ctx.Doer, + Keyword: keyword, + OrderBy: orderBy, + Private: ctx.IsSigned, + WatchedByID: ctx.Doer.ID, + Collaborate: optional.Some(false), + TopicOnly: ctx.FormBool("topic"), + IncludeDescription: setting.UI.SearchRepoDescription, + Archived: archived, + Fork: fork, + Mirror: mirror, + Template: template, + IsPrivate: private, + }) + if err != nil { + ctx.ServerError("SearchRepository", err) + return + } + total := int(count) + ctx.Data["Total"] = total + ctx.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(ctx) + if archived.Has() { + pager.AddParamString("archived", fmt.Sprint(archived.Value())) + } + if fork.Has() { + pager.AddParamString("fork", fmt.Sprint(fork.Value())) + } + if mirror.Has() { + pager.AddParamString("mirror", fmt.Sprint(mirror.Value())) + } + if template.Has() { + pager.AddParamString("template", fmt.Sprint(template.Value())) + } + if private.Has() { + pager.AddParamString("private", fmt.Sprint(private.Value())) + } + ctx.Data["Page"] = pager + + ctx.Data["Status"] = 2 + ctx.Data["Title"] = ctx.Tr("notification.watching") + + ctx.HTML(http.StatusOK, tplNotificationSubscriptions) +} + +// NewAvailable returns the notification counts +func NewAvailable(ctx *context.Context) { + total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{ + UserID: ctx.Doer.ID, + Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread}, + }) + if err != nil { + log.Error("db.Count[activities_model.Notification]", err) + ctx.JSON(http.StatusOK, structs.NotificationCount{New: 0}) + return + } + + ctx.JSON(http.StatusOK, structs.NotificationCount{New: total}) +} |