You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

notification.go 13KB

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