0
0
mirror of https://github.com/go-gitea/gitea.git synced 2025-08-02 08:15:43 +02:00
gitea/routers/web/user/notification.go
Philip Peterson c5e78fc7ad
Do not mutate incoming options to SearchRepositoryByName (#34553)
Similar to #34544, this PR changes the `opts` argument in
`SearchRepositoryByName()` to be passed by value instead of by pointer,
as its mutations do not escape the function scope and are not used
elsewhere. This simplifies reasoning about the function and avoids
unnecessary pointer usage.

This insight emerged during an initial attempt to refactor
`RenderUserSearch()`, which currently intermixes multiple concerns.

---------

Co-authored-by: Philip Peterson <philip-peterson@users.noreply.github.com>
2025-06-02 17:33:25 +00:00

445 lines
12 KiB
Go

// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"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/templates"
"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 templates.TplName = "user/notification/notification"
tplNotificationDiv templates.TplName = "user/notification/notification_div"
tplNotificationSubscriptions templates.TplName = "user/notification/notification_subscriptions"
)
// 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.AddParamFromRequest(ctx.Req)
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
switch typ {
case "reject":
reviewTyp = issues_model.ReviewTypeReject
case "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.AddParamFromRequest(ctx.Req)
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.AddParamFromRequest(ctx.Req)
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})
}