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 }