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 11KB

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