gitea源码

notification.go 12KB

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