code.gitea.io/gitea@v1.22.3/routers/web/user/notification.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package user
     5  
     6  import (
     7  	goctx "context"
     8  	"errors"
     9  	"fmt"
    10  	"net/http"
    11  	"net/url"
    12  	"strings"
    13  
    14  	activities_model "code.gitea.io/gitea/models/activities"
    15  	"code.gitea.io/gitea/models/db"
    16  	issues_model "code.gitea.io/gitea/models/issues"
    17  	repo_model "code.gitea.io/gitea/models/repo"
    18  	"code.gitea.io/gitea/modules/base"
    19  	"code.gitea.io/gitea/modules/log"
    20  	"code.gitea.io/gitea/modules/optional"
    21  	"code.gitea.io/gitea/modules/setting"
    22  	"code.gitea.io/gitea/modules/structs"
    23  	"code.gitea.io/gitea/modules/util"
    24  	"code.gitea.io/gitea/services/context"
    25  	issue_service "code.gitea.io/gitea/services/issue"
    26  	pull_service "code.gitea.io/gitea/services/pull"
    27  )
    28  
    29  const (
    30  	tplNotification              base.TplName = "user/notification/notification"
    31  	tplNotificationDiv           base.TplName = "user/notification/notification_div"
    32  	tplNotificationSubscriptions base.TplName = "user/notification/notification_subscriptions"
    33  )
    34  
    35  // GetNotificationCount is the middleware that sets the notification count in the context
    36  func GetNotificationCount(ctx *context.Context) {
    37  	if strings.HasPrefix(ctx.Req.URL.Path, "/api") {
    38  		return
    39  	}
    40  
    41  	if !ctx.IsSigned {
    42  		return
    43  	}
    44  
    45  	ctx.Data["NotificationUnreadCount"] = func() int64 {
    46  		count, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
    47  			UserID: ctx.Doer.ID,
    48  			Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
    49  		})
    50  		if err != nil {
    51  			if err != goctx.Canceled {
    52  				log.Error("Unable to GetNotificationCount for user:%-v: %v", ctx.Doer, err)
    53  			}
    54  			return -1
    55  		}
    56  
    57  		return count
    58  	}
    59  }
    60  
    61  // Notifications is the notifications page
    62  func Notifications(ctx *context.Context) {
    63  	getNotifications(ctx)
    64  	if ctx.Written() {
    65  		return
    66  	}
    67  	if ctx.FormBool("div-only") {
    68  		ctx.Data["SequenceNumber"] = ctx.FormString("sequence-number")
    69  		ctx.HTML(http.StatusOK, tplNotificationDiv)
    70  		return
    71  	}
    72  	ctx.HTML(http.StatusOK, tplNotification)
    73  }
    74  
    75  func getNotifications(ctx *context.Context) {
    76  	var (
    77  		keyword = ctx.FormTrim("q")
    78  		status  activities_model.NotificationStatus
    79  		page    = ctx.FormInt("page")
    80  		perPage = ctx.FormInt("perPage")
    81  	)
    82  	if page < 1 {
    83  		page = 1
    84  	}
    85  	if perPage < 1 {
    86  		perPage = 20
    87  	}
    88  
    89  	switch keyword {
    90  	case "read":
    91  		status = activities_model.NotificationStatusRead
    92  	default:
    93  		status = activities_model.NotificationStatusUnread
    94  	}
    95  
    96  	total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
    97  		UserID: ctx.Doer.ID,
    98  		Status: []activities_model.NotificationStatus{status},
    99  	})
   100  	if err != nil {
   101  		ctx.ServerError("ErrGetNotificationCount", err)
   102  		return
   103  	}
   104  
   105  	// redirect to last page if request page is more than total pages
   106  	pager := context.NewPagination(int(total), perPage, page, 5)
   107  	if pager.Paginater.Current() < page {
   108  		ctx.Redirect(fmt.Sprintf("%s/notifications?q=%s&page=%d", setting.AppSubURL, url.QueryEscape(ctx.FormString("q")), pager.Paginater.Current()))
   109  		return
   110  	}
   111  
   112  	statuses := []activities_model.NotificationStatus{status, activities_model.NotificationStatusPinned}
   113  	nls, err := db.Find[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
   114  		ListOptions: db.ListOptions{
   115  			PageSize: perPage,
   116  			Page:     page,
   117  		},
   118  		UserID: ctx.Doer.ID,
   119  		Status: statuses,
   120  	})
   121  	if err != nil {
   122  		ctx.ServerError("db.Find[activities_model.Notification]", err)
   123  		return
   124  	}
   125  
   126  	notifications := activities_model.NotificationList(nls)
   127  
   128  	failCount := 0
   129  
   130  	repos, failures, err := notifications.LoadRepos(ctx)
   131  	if err != nil {
   132  		ctx.ServerError("LoadRepos", err)
   133  		return
   134  	}
   135  	notifications = notifications.Without(failures)
   136  	if err := repos.LoadAttributes(ctx); err != nil {
   137  		ctx.ServerError("LoadAttributes", err)
   138  		return
   139  	}
   140  	failCount += len(failures)
   141  
   142  	failures, err = notifications.LoadIssues(ctx)
   143  	if err != nil {
   144  		ctx.ServerError("LoadIssues", err)
   145  		return
   146  	}
   147  
   148  	if err = notifications.LoadIssuePullRequests(ctx); err != nil {
   149  		ctx.ServerError("LoadIssuePullRequests", err)
   150  		return
   151  	}
   152  
   153  	notifications = notifications.Without(failures)
   154  	failCount += len(failures)
   155  
   156  	failures, err = notifications.LoadComments(ctx)
   157  	if err != nil {
   158  		ctx.ServerError("LoadComments", err)
   159  		return
   160  	}
   161  	notifications = notifications.Without(failures)
   162  	failCount += len(failures)
   163  
   164  	if failCount > 0 {
   165  		ctx.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount))
   166  	}
   167  
   168  	ctx.Data["Title"] = ctx.Tr("notifications")
   169  	ctx.Data["Keyword"] = keyword
   170  	ctx.Data["Status"] = status
   171  	ctx.Data["Notifications"] = notifications
   172  
   173  	pager.SetDefaultParams(ctx)
   174  	ctx.Data["Page"] = pager
   175  }
   176  
   177  // NotificationStatusPost is a route for changing the status of a notification
   178  func NotificationStatusPost(ctx *context.Context) {
   179  	var (
   180  		notificationID = ctx.FormInt64("notification_id")
   181  		statusStr      = ctx.FormString("status")
   182  		status         activities_model.NotificationStatus
   183  	)
   184  
   185  	switch statusStr {
   186  	case "read":
   187  		status = activities_model.NotificationStatusRead
   188  	case "unread":
   189  		status = activities_model.NotificationStatusUnread
   190  	case "pinned":
   191  		status = activities_model.NotificationStatusPinned
   192  	default:
   193  		ctx.ServerError("InvalidNotificationStatus", errors.New("Invalid notification status"))
   194  		return
   195  	}
   196  
   197  	if _, err := activities_model.SetNotificationStatus(ctx, notificationID, ctx.Doer, status); err != nil {
   198  		ctx.ServerError("SetNotificationStatus", err)
   199  		return
   200  	}
   201  
   202  	if !ctx.FormBool("noredirect") {
   203  		url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, url.QueryEscape(ctx.FormString("page")))
   204  		ctx.Redirect(url, http.StatusSeeOther)
   205  	}
   206  
   207  	getNotifications(ctx)
   208  	if ctx.Written() {
   209  		return
   210  	}
   211  	ctx.Data["Link"] = setting.AppSubURL + "/notifications"
   212  	ctx.Data["SequenceNumber"] = ctx.Req.PostFormValue("sequence-number")
   213  
   214  	ctx.HTML(http.StatusOK, tplNotificationDiv)
   215  }
   216  
   217  // NotificationPurgePost is a route for 'purging' the list of notifications - marking all unread as read
   218  func NotificationPurgePost(ctx *context.Context) {
   219  	err := activities_model.UpdateNotificationStatuses(ctx, ctx.Doer, activities_model.NotificationStatusUnread, activities_model.NotificationStatusRead)
   220  	if err != nil {
   221  		ctx.ServerError("UpdateNotificationStatuses", err)
   222  		return
   223  	}
   224  
   225  	ctx.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther)
   226  }
   227  
   228  // NotificationSubscriptions returns the list of subscribed issues
   229  func NotificationSubscriptions(ctx *context.Context) {
   230  	page := ctx.FormInt("page")
   231  	if page < 1 {
   232  		page = 1
   233  	}
   234  
   235  	sortType := ctx.FormString("sort")
   236  	ctx.Data["SortType"] = sortType
   237  
   238  	state := ctx.FormString("state")
   239  	if !util.SliceContainsString([]string{"all", "open", "closed"}, state, true) {
   240  		state = "all"
   241  	}
   242  
   243  	ctx.Data["State"] = state
   244  	// default state filter is "all"
   245  	showClosed := optional.None[bool]()
   246  	switch state {
   247  	case "closed":
   248  		showClosed = optional.Some(true)
   249  	case "open":
   250  		showClosed = optional.Some(false)
   251  	}
   252  
   253  	issueType := ctx.FormString("issueType")
   254  	// default issue type is no filter
   255  	issueTypeBool := optional.None[bool]()
   256  	switch issueType {
   257  	case "issues":
   258  		issueTypeBool = optional.Some(false)
   259  	case "pulls":
   260  		issueTypeBool = optional.Some(true)
   261  	}
   262  	ctx.Data["IssueType"] = issueType
   263  
   264  	var labelIDs []int64
   265  	selectedLabels := ctx.FormString("labels")
   266  	ctx.Data["Labels"] = selectedLabels
   267  	if len(selectedLabels) > 0 && selectedLabels != "0" {
   268  		var err error
   269  		labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
   270  		if err != nil {
   271  			ctx.Flash.Error(ctx.Tr("invalid_data", selectedLabels), true)
   272  		}
   273  	}
   274  
   275  	count, err := issues_model.CountIssues(ctx, &issues_model.IssuesOptions{
   276  		SubscriberID: ctx.Doer.ID,
   277  		IsClosed:     showClosed,
   278  		IsPull:       issueTypeBool,
   279  		LabelIDs:     labelIDs,
   280  	})
   281  	if err != nil {
   282  		ctx.ServerError("CountIssues", err)
   283  		return
   284  	}
   285  	issues, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
   286  		Paginator: &db.ListOptions{
   287  			PageSize: setting.UI.IssuePagingNum,
   288  			Page:     page,
   289  		},
   290  		SubscriberID: ctx.Doer.ID,
   291  		SortType:     sortType,
   292  		IsClosed:     showClosed,
   293  		IsPull:       issueTypeBool,
   294  		LabelIDs:     labelIDs,
   295  	})
   296  	if err != nil {
   297  		ctx.ServerError("Issues", err)
   298  		return
   299  	}
   300  
   301  	commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
   302  	if err != nil {
   303  		ctx.ServerError("GetIssuesAllCommitStatus", err)
   304  		return
   305  	}
   306  	ctx.Data["CommitLastStatus"] = lastStatus
   307  	ctx.Data["CommitStatuses"] = commitStatuses
   308  	ctx.Data["Issues"] = issues
   309  
   310  	ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, "")
   311  
   312  	commitStatus, err := pull_service.GetIssuesLastCommitStatus(ctx, issues)
   313  	if err != nil {
   314  		ctx.ServerError("GetIssuesLastCommitStatus", err)
   315  		return
   316  	}
   317  	ctx.Data["CommitStatus"] = commitStatus
   318  
   319  	approvalCounts, err := issues.GetApprovalCounts(ctx)
   320  	if err != nil {
   321  		ctx.ServerError("ApprovalCounts", err)
   322  		return
   323  	}
   324  	ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
   325  		counts, ok := approvalCounts[issueID]
   326  		if !ok || len(counts) == 0 {
   327  			return 0
   328  		}
   329  		reviewTyp := issues_model.ReviewTypeApprove
   330  		if typ == "reject" {
   331  			reviewTyp = issues_model.ReviewTypeReject
   332  		} else if typ == "waiting" {
   333  			reviewTyp = issues_model.ReviewTypeRequest
   334  		}
   335  		for _, count := range counts {
   336  			if count.Type == reviewTyp {
   337  				return count.Count
   338  			}
   339  		}
   340  		return 0
   341  	}
   342  
   343  	ctx.Data["Status"] = 1
   344  	ctx.Data["Title"] = ctx.Tr("notification.subscriptions")
   345  
   346  	// redirect to last page if request page is more than total pages
   347  	pager := context.NewPagination(int(count), setting.UI.IssuePagingNum, page, 5)
   348  	if pager.Paginater.Current() < page {
   349  		ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current()))
   350  		return
   351  	}
   352  	pager.AddParamString("sort", sortType)
   353  	pager.AddParamString("state", state)
   354  	ctx.Data["Page"] = pager
   355  
   356  	ctx.HTML(http.StatusOK, tplNotificationSubscriptions)
   357  }
   358  
   359  // NotificationWatching returns the list of watching repos
   360  func NotificationWatching(ctx *context.Context) {
   361  	page := ctx.FormInt("page")
   362  	if page < 1 {
   363  		page = 1
   364  	}
   365  
   366  	keyword := ctx.FormTrim("q")
   367  	ctx.Data["Keyword"] = keyword
   368  
   369  	var orderBy db.SearchOrderBy
   370  	ctx.Data["SortType"] = ctx.FormString("sort")
   371  	switch ctx.FormString("sort") {
   372  	case "newest":
   373  		orderBy = db.SearchOrderByNewest
   374  	case "oldest":
   375  		orderBy = db.SearchOrderByOldest
   376  	case "recentupdate":
   377  		orderBy = db.SearchOrderByRecentUpdated
   378  	case "leastupdate":
   379  		orderBy = db.SearchOrderByLeastUpdated
   380  	case "reversealphabetically":
   381  		orderBy = db.SearchOrderByAlphabeticallyReverse
   382  	case "alphabetically":
   383  		orderBy = db.SearchOrderByAlphabetically
   384  	case "moststars":
   385  		orderBy = db.SearchOrderByStarsReverse
   386  	case "feweststars":
   387  		orderBy = db.SearchOrderByStars
   388  	case "mostforks":
   389  		orderBy = db.SearchOrderByForksReverse
   390  	case "fewestforks":
   391  		orderBy = db.SearchOrderByForks
   392  	default:
   393  		ctx.Data["SortType"] = "recentupdate"
   394  		orderBy = db.SearchOrderByRecentUpdated
   395  	}
   396  
   397  	archived := ctx.FormOptionalBool("archived")
   398  	ctx.Data["IsArchived"] = archived
   399  
   400  	fork := ctx.FormOptionalBool("fork")
   401  	ctx.Data["IsFork"] = fork
   402  
   403  	mirror := ctx.FormOptionalBool("mirror")
   404  	ctx.Data["IsMirror"] = mirror
   405  
   406  	template := ctx.FormOptionalBool("template")
   407  	ctx.Data["IsTemplate"] = template
   408  
   409  	private := ctx.FormOptionalBool("private")
   410  	ctx.Data["IsPrivate"] = private
   411  
   412  	repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
   413  		ListOptions: db.ListOptions{
   414  			PageSize: setting.UI.User.RepoPagingNum,
   415  			Page:     page,
   416  		},
   417  		Actor:              ctx.Doer,
   418  		Keyword:            keyword,
   419  		OrderBy:            orderBy,
   420  		Private:            ctx.IsSigned,
   421  		WatchedByID:        ctx.Doer.ID,
   422  		Collaborate:        optional.Some(false),
   423  		TopicOnly:          ctx.FormBool("topic"),
   424  		IncludeDescription: setting.UI.SearchRepoDescription,
   425  		Archived:           archived,
   426  		Fork:               fork,
   427  		Mirror:             mirror,
   428  		Template:           template,
   429  		IsPrivate:          private,
   430  	})
   431  	if err != nil {
   432  		ctx.ServerError("SearchRepository", err)
   433  		return
   434  	}
   435  	total := int(count)
   436  	ctx.Data["Total"] = total
   437  	ctx.Data["Repos"] = repos
   438  
   439  	// redirect to last page if request page is more than total pages
   440  	pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5)
   441  	pager.SetDefaultParams(ctx)
   442  	if archived.Has() {
   443  		pager.AddParamString("archived", fmt.Sprint(archived.Value()))
   444  	}
   445  	if fork.Has() {
   446  		pager.AddParamString("fork", fmt.Sprint(fork.Value()))
   447  	}
   448  	if mirror.Has() {
   449  		pager.AddParamString("mirror", fmt.Sprint(mirror.Value()))
   450  	}
   451  	if template.Has() {
   452  		pager.AddParamString("template", fmt.Sprint(template.Value()))
   453  	}
   454  	if private.Has() {
   455  		pager.AddParamString("private", fmt.Sprint(private.Value()))
   456  	}
   457  	ctx.Data["Page"] = pager
   458  
   459  	ctx.Data["Status"] = 2
   460  	ctx.Data["Title"] = ctx.Tr("notification.watching")
   461  
   462  	ctx.HTML(http.StatusOK, tplNotificationSubscriptions)
   463  }
   464  
   465  // NewAvailable returns the notification counts
   466  func NewAvailable(ctx *context.Context) {
   467  	total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
   468  		UserID: ctx.Doer.ID,
   469  		Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
   470  	})
   471  	if err != nil {
   472  		log.Error("db.Count[activities_model.Notification]", err)
   473  		ctx.JSON(http.StatusOK, structs.NotificationCount{New: 0})
   474  		return
   475  	}
   476  
   477  	ctx.JSON(http.StatusOK, structs.NotificationCount{New: total})
   478  }