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 }