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