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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  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. notifications = notifications.Without(failures)
  130. failCount += len(failures)
  131. failures, err = notifications.LoadComments(ctx)
  132. if err != nil {
  133. ctx.ServerError("LoadComments", err)
  134. return
  135. }
  136. notifications = notifications.Without(failures)
  137. failCount += len(failures)
  138. if failCount > 0 {
  139. ctx.Flash.Error(fmt.Sprintf("ERROR: %d notifications were removed due to missing parts - check the logs", failCount))
  140. }
  141. ctx.Data["Title"] = ctx.Tr("notifications")
  142. ctx.Data["Keyword"] = keyword
  143. ctx.Data["Status"] = status
  144. ctx.Data["Notifications"] = notifications
  145. pager.SetDefaultParams(ctx)
  146. ctx.Data["Page"] = pager
  147. }
  148. // NotificationStatusPost is a route for changing the status of a notification
  149. func NotificationStatusPost(ctx *context.Context) {
  150. var (
  151. notificationID = ctx.FormInt64("notification_id")
  152. statusStr = ctx.FormString("status")
  153. status activities_model.NotificationStatus
  154. )
  155. switch statusStr {
  156. case "read":
  157. status = activities_model.NotificationStatusRead
  158. case "unread":
  159. status = activities_model.NotificationStatusUnread
  160. case "pinned":
  161. status = activities_model.NotificationStatusPinned
  162. default:
  163. ctx.ServerError("InvalidNotificationStatus", errors.New("Invalid notification status"))
  164. return
  165. }
  166. if _, err := activities_model.SetNotificationStatus(ctx, notificationID, ctx.Doer, status); err != nil {
  167. ctx.ServerError("SetNotificationStatus", err)
  168. return
  169. }
  170. if !ctx.FormBool("noredirect") {
  171. url := fmt.Sprintf("%s/notifications?page=%s", setting.AppSubURL, url.QueryEscape(ctx.FormString("page")))
  172. ctx.Redirect(url, http.StatusSeeOther)
  173. }
  174. getNotifications(ctx)
  175. if ctx.Written() {
  176. return
  177. }
  178. ctx.Data["Link"] = setting.AppSubURL + "/notifications"
  179. ctx.Data["SequenceNumber"] = ctx.Req.PostFormValue("sequence-number")
  180. ctx.HTML(http.StatusOK, tplNotificationDiv)
  181. }
  182. // NotificationPurgePost is a route for 'purging' the list of notifications - marking all unread as read
  183. func NotificationPurgePost(ctx *context.Context) {
  184. err := activities_model.UpdateNotificationStatuses(ctx, ctx.Doer, activities_model.NotificationStatusUnread, activities_model.NotificationStatusRead)
  185. if err != nil {
  186. ctx.ServerError("UpdateNotificationStatuses", err)
  187. return
  188. }
  189. ctx.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther)
  190. }
  191. // NotificationSubscriptions returns the list of subscribed issues
  192. func NotificationSubscriptions(ctx *context.Context) {
  193. page := ctx.FormInt("page")
  194. if page < 1 {
  195. page = 1
  196. }
  197. sortType := ctx.FormString("sort")
  198. ctx.Data["SortType"] = sortType
  199. state := ctx.FormString("state")
  200. if !util.SliceContainsString([]string{"all", "open", "closed"}, state, true) {
  201. state = "all"
  202. }
  203. ctx.Data["State"] = state
  204. // default state filter is "all"
  205. showClosed := optional.None[bool]()
  206. switch state {
  207. case "closed":
  208. showClosed = optional.Some(true)
  209. case "open":
  210. showClosed = optional.Some(false)
  211. }
  212. issueType := ctx.FormString("issueType")
  213. // default issue type is no filter
  214. issueTypeBool := optional.None[bool]()
  215. switch issueType {
  216. case "issues":
  217. issueTypeBool = optional.Some(false)
  218. case "pulls":
  219. issueTypeBool = optional.Some(true)
  220. }
  221. ctx.Data["IssueType"] = issueType
  222. var labelIDs []int64
  223. selectedLabels := ctx.FormString("labels")
  224. ctx.Data["Labels"] = selectedLabels
  225. if len(selectedLabels) > 0 && selectedLabels != "0" {
  226. var err error
  227. labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ","))
  228. if err != nil {
  229. ctx.ServerError("StringsToInt64s", err)
  230. return
  231. }
  232. }
  233. count, err := issues_model.CountIssues(ctx, &issues_model.IssuesOptions{
  234. SubscriberID: ctx.Doer.ID,
  235. IsClosed: showClosed,
  236. IsPull: issueTypeBool,
  237. LabelIDs: labelIDs,
  238. })
  239. if err != nil {
  240. ctx.ServerError("CountIssues", err)
  241. return
  242. }
  243. issues, err := issues_model.Issues(ctx, &issues_model.IssuesOptions{
  244. Paginator: &db.ListOptions{
  245. PageSize: setting.UI.IssuePagingNum,
  246. Page: page,
  247. },
  248. SubscriberID: ctx.Doer.ID,
  249. SortType: sortType,
  250. IsClosed: showClosed,
  251. IsPull: issueTypeBool,
  252. LabelIDs: labelIDs,
  253. })
  254. if err != nil {
  255. ctx.ServerError("Issues", err)
  256. return
  257. }
  258. commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(ctx, issues)
  259. if err != nil {
  260. ctx.ServerError("GetIssuesAllCommitStatus", err)
  261. return
  262. }
  263. ctx.Data["CommitLastStatus"] = lastStatus
  264. ctx.Data["CommitStatuses"] = commitStatuses
  265. ctx.Data["Issues"] = issues
  266. ctx.Data["IssueRefEndNames"], ctx.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, "")
  267. commitStatus, err := pull_service.GetIssuesLastCommitStatus(ctx, issues)
  268. if err != nil {
  269. ctx.ServerError("GetIssuesLastCommitStatus", err)
  270. return
  271. }
  272. ctx.Data["CommitStatus"] = commitStatus
  273. approvalCounts, err := issues.GetApprovalCounts(ctx)
  274. if err != nil {
  275. ctx.ServerError("ApprovalCounts", err)
  276. return
  277. }
  278. ctx.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 {
  279. counts, ok := approvalCounts[issueID]
  280. if !ok || len(counts) == 0 {
  281. return 0
  282. }
  283. reviewTyp := issues_model.ReviewTypeApprove
  284. if typ == "reject" {
  285. reviewTyp = issues_model.ReviewTypeReject
  286. } else if typ == "waiting" {
  287. reviewTyp = issues_model.ReviewTypeRequest
  288. }
  289. for _, count := range counts {
  290. if count.Type == reviewTyp {
  291. return count.Count
  292. }
  293. }
  294. return 0
  295. }
  296. ctx.Data["Status"] = 1
  297. ctx.Data["Title"] = ctx.Tr("notification.subscriptions")
  298. // redirect to last page if request page is more than total pages
  299. pager := context.NewPagination(int(count), setting.UI.IssuePagingNum, page, 5)
  300. if pager.Paginater.Current() < page {
  301. ctx.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current()))
  302. return
  303. }
  304. pager.AddParam(ctx, "sort", "SortType")
  305. pager.AddParam(ctx, "state", "State")
  306. ctx.Data["Page"] = pager
  307. ctx.HTML(http.StatusOK, tplNotificationSubscriptions)
  308. }
  309. // NotificationWatching returns the list of watching repos
  310. func NotificationWatching(ctx *context.Context) {
  311. page := ctx.FormInt("page")
  312. if page < 1 {
  313. page = 1
  314. }
  315. keyword := ctx.FormTrim("q")
  316. ctx.Data["Keyword"] = keyword
  317. var orderBy db.SearchOrderBy
  318. ctx.Data["SortType"] = ctx.FormString("sort")
  319. switch ctx.FormString("sort") {
  320. case "newest":
  321. orderBy = db.SearchOrderByNewest
  322. case "oldest":
  323. orderBy = db.SearchOrderByOldest
  324. case "recentupdate":
  325. orderBy = db.SearchOrderByRecentUpdated
  326. case "leastupdate":
  327. orderBy = db.SearchOrderByLeastUpdated
  328. case "reversealphabetically":
  329. orderBy = db.SearchOrderByAlphabeticallyReverse
  330. case "alphabetically":
  331. orderBy = db.SearchOrderByAlphabetically
  332. case "moststars":
  333. orderBy = db.SearchOrderByStarsReverse
  334. case "feweststars":
  335. orderBy = db.SearchOrderByStars
  336. case "mostforks":
  337. orderBy = db.SearchOrderByForksReverse
  338. case "fewestforks":
  339. orderBy = db.SearchOrderByForks
  340. default:
  341. ctx.Data["SortType"] = "recentupdate"
  342. orderBy = db.SearchOrderByRecentUpdated
  343. }
  344. archived := ctx.FormOptionalBool("archived")
  345. ctx.Data["IsArchived"] = archived
  346. fork := ctx.FormOptionalBool("fork")
  347. ctx.Data["IsFork"] = fork
  348. mirror := ctx.FormOptionalBool("mirror")
  349. ctx.Data["IsMirror"] = mirror
  350. template := ctx.FormOptionalBool("template")
  351. ctx.Data["IsTemplate"] = template
  352. private := ctx.FormOptionalBool("private")
  353. ctx.Data["IsPrivate"] = private
  354. repos, count, err := repo_model.SearchRepository(ctx, &repo_model.SearchRepoOptions{
  355. ListOptions: db.ListOptions{
  356. PageSize: setting.UI.User.RepoPagingNum,
  357. Page: page,
  358. },
  359. Actor: ctx.Doer,
  360. Keyword: keyword,
  361. OrderBy: orderBy,
  362. Private: ctx.IsSigned,
  363. WatchedByID: ctx.Doer.ID,
  364. Collaborate: optional.Some(false),
  365. TopicOnly: ctx.FormBool("topic"),
  366. IncludeDescription: setting.UI.SearchRepoDescription,
  367. Archived: archived,
  368. Fork: fork,
  369. Mirror: mirror,
  370. Template: template,
  371. IsPrivate: private,
  372. })
  373. if err != nil {
  374. ctx.ServerError("SearchRepository", err)
  375. return
  376. }
  377. total := int(count)
  378. ctx.Data["Total"] = total
  379. ctx.Data["Repos"] = repos
  380. // redirect to last page if request page is more than total pages
  381. pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5)
  382. pager.SetDefaultParams(ctx)
  383. ctx.Data["Page"] = pager
  384. ctx.Data["Status"] = 2
  385. ctx.Data["Title"] = ctx.Tr("notification.watching")
  386. ctx.HTML(http.StatusOK, tplNotificationSubscriptions)
  387. }
  388. // NewAvailable returns the notification counts
  389. func NewAvailable(ctx *context.Context) {
  390. total, err := db.Count[activities_model.Notification](ctx, activities_model.FindNotificationOptions{
  391. UserID: ctx.Doer.ID,
  392. Status: []activities_model.NotificationStatus{activities_model.NotificationStatusUnread},
  393. })
  394. if err != nil {
  395. log.Error("db.Count[activities_model.Notification]", err)
  396. ctx.JSON(http.StatusOK, structs.NotificationCount{New: 0})
  397. return
  398. }
  399. ctx.JSON(http.StatusOK, structs.NotificationCount{New: total})
  400. }