mirror of
				https://codeberg.org/forgejo/forgejo.git
				synced 2025-11-04 00:11:04 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			463 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			463 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// 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"
 | 
						|
	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/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
 | 
						|
	}
 | 
						|
	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)
 | 
						|
	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})
 | 
						|
}
 |