mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 03:18:24 +00:00 
			
		
		
		
	Add pages to view watched repos and subscribed issues/PRs (#17156)
Adds GitHub-like pages to view watched repos and subscribed issues/PRs This is my second try to fix this, but it is better than the first since it doesn't uses a filter option which could be slow when accessing `/issues` or `/pulls` and it shows both pulls and issues (the first try is #17053). Closes #16111 Replaces and closes #17053  Co-authored-by: Lauris BH <lauris@nix.lv> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		| @@ -1186,6 +1186,7 @@ type IssuesOptions struct { //nolint | ||||
| 	PosterID           int64 | ||||
| 	MentionedID        int64 | ||||
| 	ReviewRequestedID  int64 | ||||
| 	SubscriberID       int64 | ||||
| 	MilestoneIDs       []int64 | ||||
| 	ProjectID          int64 | ||||
| 	ProjectBoardID     int64 | ||||
| @@ -1299,6 +1300,10 @@ func (opts *IssuesOptions) setupSessionNoLimit(sess *xorm.Session) { | ||||
| 		applyReviewRequestedCondition(sess, opts.ReviewRequestedID) | ||||
| 	} | ||||
|  | ||||
| 	if opts.SubscriberID > 0 { | ||||
| 		applySubscribedCondition(sess, opts.SubscriberID) | ||||
| 	} | ||||
|  | ||||
| 	if len(opts.MilestoneIDs) > 0 { | ||||
| 		sess.In("issue.milestone_id", opts.MilestoneIDs) | ||||
| 	} | ||||
| @@ -1463,6 +1468,36 @@ func applyReviewRequestedCondition(sess *xorm.Session, reviewRequestedID int64) | ||||
| 			reviewRequestedID, ReviewTypeApprove, ReviewTypeReject, ReviewTypeRequest, reviewRequestedID) | ||||
| } | ||||
|  | ||||
| func applySubscribedCondition(sess *xorm.Session, subscriberID int64) *xorm.Session { | ||||
| 	return sess.And( | ||||
| 		builder. | ||||
| 			NotIn("issue.id", | ||||
| 				builder.Select("issue_id"). | ||||
| 					From("issue_watch"). | ||||
| 					Where(builder.Eq{"is_watching": false, "user_id": subscriberID}), | ||||
| 			), | ||||
| 	).And( | ||||
| 		builder.Or( | ||||
| 			builder.In("issue.id", builder. | ||||
| 				Select("issue_id"). | ||||
| 				From("issue_watch"). | ||||
| 				Where(builder.Eq{"is_watching": true, "user_id": subscriberID}), | ||||
| 			), | ||||
| 			builder.In("issue.id", builder. | ||||
| 				Select("issue_id"). | ||||
| 				From("comment"). | ||||
| 				Where(builder.Eq{"poster_id": subscriberID}), | ||||
| 			), | ||||
| 			builder.Eq{"issue.poster_id": subscriberID}, | ||||
| 			builder.In("issue.repo_id", builder. | ||||
| 				Select("id"). | ||||
| 				From("watch"). | ||||
| 				Where(builder.Eq{"user_id": subscriberID, "mode": true}), | ||||
| 			), | ||||
| 		), | ||||
| 	) | ||||
| } | ||||
|  | ||||
| // CountIssuesByRepo map from repoID to number of issues matching the options | ||||
| func CountIssuesByRepo(opts *IssuesOptions) (map[int64]int64, error) { | ||||
| 	e := db.GetEngine(db.DefaultContext) | ||||
|   | ||||
| @@ -3034,6 +3034,9 @@ pin = Pin notification | ||||
| mark_as_read = Mark as read | ||||
| mark_as_unread = Mark as unread | ||||
| mark_all_as_read = Mark all as read | ||||
| subscriptions = Subscriptions | ||||
| watching = Watching | ||||
| no_subscriptions = No subscriptions | ||||
|  | ||||
| [gpg] | ||||
| default_key=Signed with default key | ||||
|   | ||||
| @@ -13,16 +13,23 @@ import ( | ||||
| 	"strings" | ||||
|  | ||||
| 	activities_model "code.gitea.io/gitea/models/activities" | ||||
| 	"code.gitea.io/gitea/models/db" | ||||
| 	issues_model "code.gitea.io/gitea/models/issues" | ||||
| 	repo_model "code.gitea.io/gitea/models/repo" | ||||
| 	"code.gitea.io/gitea/modules/base" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/structs" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	issue_service "code.gitea.io/gitea/services/issue" | ||||
| 	pull_service "code.gitea.io/gitea/services/pull" | ||||
| ) | ||||
|  | ||||
| const ( | ||||
| 	tplNotification    base.TplName = "user/notification/notification" | ||||
| 	tplNotificationDiv base.TplName = "user/notification/notification_div" | ||||
| 	tplNotification              base.TplName = "user/notification/notification" | ||||
| 	tplNotificationDiv           base.TplName = "user/notification/notification_div" | ||||
| 	tplNotificationSubscriptions base.TplName = "user/notification/notification_subscriptions" | ||||
| ) | ||||
|  | ||||
| // GetNotificationCount is the middleware that sets the notification count in the context | ||||
| @@ -197,6 +204,208 @@ func NotificationPurgePost(c *context.Context) { | ||||
| 	c.Redirect(setting.AppSubURL+"/notifications", http.StatusSeeOther) | ||||
| } | ||||
|  | ||||
| // NotificationSubscriptions returns the list of subscribed issues | ||||
| func NotificationSubscriptions(c *context.Context) { | ||||
| 	page := c.FormInt("page") | ||||
| 	if page < 1 { | ||||
| 		page = 1 | ||||
| 	} | ||||
|  | ||||
| 	sortType := c.FormString("sort") | ||||
| 	c.Data["SortType"] = sortType | ||||
|  | ||||
| 	state := c.FormString("state") | ||||
| 	if !util.IsStringInSlice(state, []string{"all", "open", "closed"}, true) { | ||||
| 		state = "all" | ||||
| 	} | ||||
| 	c.Data["State"] = state | ||||
| 	var showClosed util.OptionalBool | ||||
| 	switch state { | ||||
| 	case "all": | ||||
| 		showClosed = util.OptionalBoolNone | ||||
| 	case "closed": | ||||
| 		showClosed = util.OptionalBoolTrue | ||||
| 	case "open": | ||||
| 		showClosed = util.OptionalBoolFalse | ||||
| 	} | ||||
|  | ||||
| 	var issueTypeBool util.OptionalBool | ||||
| 	issueType := c.FormString("issueType") | ||||
| 	switch issueType { | ||||
| 	case "issues": | ||||
| 		issueTypeBool = util.OptionalBoolFalse | ||||
| 	case "pulls": | ||||
| 		issueTypeBool = util.OptionalBoolTrue | ||||
| 	default: | ||||
| 		issueTypeBool = util.OptionalBoolNone | ||||
| 	} | ||||
| 	c.Data["IssueType"] = issueType | ||||
|  | ||||
| 	var labelIDs []int64 | ||||
| 	selectedLabels := c.FormString("labels") | ||||
| 	c.Data["Labels"] = selectedLabels | ||||
| 	if len(selectedLabels) > 0 && selectedLabels != "0" { | ||||
| 		var err error | ||||
| 		labelIDs, err = base.StringsToInt64s(strings.Split(selectedLabels, ",")) | ||||
| 		if err != nil { | ||||
| 			c.ServerError("StringsToInt64s", err) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	count, err := issues_model.CountIssues(&issues_model.IssuesOptions{ | ||||
| 		SubscriberID: c.Doer.ID, | ||||
| 		IsClosed:     showClosed, | ||||
| 		IsPull:       issueTypeBool, | ||||
| 		LabelIDs:     labelIDs, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		c.ServerError("CountIssues", err) | ||||
| 		return | ||||
| 	} | ||||
| 	issues, err := issues_model.Issues(&issues_model.IssuesOptions{ | ||||
| 		ListOptions: db.ListOptions{ | ||||
| 			PageSize: setting.UI.IssuePagingNum, | ||||
| 			Page:     page, | ||||
| 		}, | ||||
| 		SubscriberID: c.Doer.ID, | ||||
| 		SortType:     sortType, | ||||
| 		IsClosed:     showClosed, | ||||
| 		IsPull:       issueTypeBool, | ||||
| 		LabelIDs:     labelIDs, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		c.ServerError("Issues", err) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	commitStatuses, lastStatus, err := pull_service.GetIssuesAllCommitStatus(c, issues) | ||||
| 	if err != nil { | ||||
| 		c.ServerError("GetIssuesAllCommitStatus", err) | ||||
| 		return | ||||
| 	} | ||||
| 	c.Data["CommitLastStatus"] = lastStatus | ||||
| 	c.Data["CommitStatuses"] = commitStatuses | ||||
| 	c.Data["Issues"] = issues | ||||
|  | ||||
| 	c.Data["IssueRefEndNames"], c.Data["IssueRefURLs"] = issue_service.GetRefEndNamesAndURLs(issues, "") | ||||
|  | ||||
| 	commitStatus, err := pull_service.GetIssuesLastCommitStatus(c, issues) | ||||
| 	if err != nil { | ||||
| 		c.ServerError("GetIssuesLastCommitStatus", err) | ||||
| 		return | ||||
| 	} | ||||
| 	c.Data["CommitStatus"] = commitStatus | ||||
|  | ||||
| 	issueList := issues_model.IssueList(issues) | ||||
| 	approvalCounts, err := issueList.GetApprovalCounts(c) | ||||
| 	if err != nil { | ||||
| 		c.ServerError("ApprovalCounts", err) | ||||
| 		return | ||||
| 	} | ||||
| 	c.Data["ApprovalCounts"] = func(issueID int64, typ string) int64 { | ||||
| 		counts, ok := approvalCounts[issueID] | ||||
| 		if !ok || len(counts) == 0 { | ||||
| 			return 0 | ||||
| 		} | ||||
| 		reviewTyp := issues_model.ReviewTypeApprove | ||||
| 		if typ == "reject" { | ||||
| 			reviewTyp = issues_model.ReviewTypeReject | ||||
| 		} else if typ == "waiting" { | ||||
| 			reviewTyp = issues_model.ReviewTypeRequest | ||||
| 		} | ||||
| 		for _, count := range counts { | ||||
| 			if count.Type == reviewTyp { | ||||
| 				return count.Count | ||||
| 			} | ||||
| 		} | ||||
| 		return 0 | ||||
| 	} | ||||
|  | ||||
| 	c.Data["Status"] = 1 | ||||
| 	c.Data["Title"] = c.Tr("notification.subscriptions") | ||||
|  | ||||
| 	// redirect to last page if request page is more than total pages | ||||
| 	pager := context.NewPagination(int(count), setting.UI.IssuePagingNum, page, 5) | ||||
| 	if pager.Paginater.Current() < page { | ||||
| 		c.Redirect(fmt.Sprintf("/notifications/subscriptions?page=%d", pager.Paginater.Current())) | ||||
| 		return | ||||
| 	} | ||||
| 	pager.AddParam(c, "sort", "SortType") | ||||
| 	pager.AddParam(c, "state", "State") | ||||
| 	c.Data["Page"] = pager | ||||
|  | ||||
| 	c.HTML(http.StatusOK, tplNotificationSubscriptions) | ||||
| } | ||||
|  | ||||
| // NotificationWatching returns the list of watching repos | ||||
| func NotificationWatching(c *context.Context) { | ||||
| 	page := c.FormInt("page") | ||||
| 	if page < 1 { | ||||
| 		page = 1 | ||||
| 	} | ||||
|  | ||||
| 	var orderBy db.SearchOrderBy | ||||
| 	c.Data["SortType"] = c.FormString("sort") | ||||
| 	switch c.FormString("sort") { | ||||
| 	case "newest": | ||||
| 		orderBy = db.SearchOrderByNewest | ||||
| 	case "oldest": | ||||
| 		orderBy = db.SearchOrderByOldest | ||||
| 	case "recentupdate": | ||||
| 		orderBy = db.SearchOrderByRecentUpdated | ||||
| 	case "leastupdate": | ||||
| 		orderBy = db.SearchOrderByLeastUpdated | ||||
| 	case "reversealphabetically": | ||||
| 		orderBy = db.SearchOrderByAlphabeticallyReverse | ||||
| 	case "alphabetically": | ||||
| 		orderBy = db.SearchOrderByAlphabetically | ||||
| 	case "moststars": | ||||
| 		orderBy = db.SearchOrderByStarsReverse | ||||
| 	case "feweststars": | ||||
| 		orderBy = db.SearchOrderByStars | ||||
| 	case "mostforks": | ||||
| 		orderBy = db.SearchOrderByForksReverse | ||||
| 	case "fewestforks": | ||||
| 		orderBy = db.SearchOrderByForks | ||||
| 	default: | ||||
| 		c.Data["SortType"] = "recentupdate" | ||||
| 		orderBy = db.SearchOrderByRecentUpdated | ||||
| 	} | ||||
|  | ||||
| 	repos, count, err := repo_model.SearchRepository(&repo_model.SearchRepoOptions{ | ||||
| 		ListOptions: db.ListOptions{ | ||||
| 			PageSize: setting.UI.User.RepoPagingNum, | ||||
| 			Page:     page, | ||||
| 		}, | ||||
| 		Actor:              c.Doer, | ||||
| 		Keyword:            c.FormTrim("q"), | ||||
| 		OrderBy:            orderBy, | ||||
| 		Private:            c.IsSigned, | ||||
| 		WatchedByID:        c.Doer.ID, | ||||
| 		Collaborate:        util.OptionalBoolFalse, | ||||
| 		TopicOnly:          c.FormBool("topic"), | ||||
| 		IncludeDescription: setting.UI.SearchRepoDescription, | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		c.ServerError("ErrSearchRepository", err) | ||||
| 		return | ||||
| 	} | ||||
| 	total := int(count) | ||||
| 	c.Data["Total"] = total | ||||
| 	c.Data["Repos"] = repos | ||||
|  | ||||
| 	// redirect to last page if request page is more than total pages | ||||
| 	pager := context.NewPagination(total, setting.UI.User.RepoPagingNum, page, 5) | ||||
| 	pager.SetDefaultParams(c) | ||||
| 	c.Data["Page"] = pager | ||||
|  | ||||
| 	c.Data["Status"] = 2 | ||||
| 	c.Data["Title"] = c.Tr("notification.watching") | ||||
|  | ||||
| 	c.HTML(http.StatusOK, tplNotificationSubscriptions) | ||||
| } | ||||
|  | ||||
| // NewAvailable returns the notification counts | ||||
| func NewAvailable(ctx *context.Context) { | ||||
| 	ctx.JSON(http.StatusOK, structs.NotificationCount{New: activities_model.CountUnread(ctx, ctx.Doer.ID)}) | ||||
|   | ||||
| @@ -1269,6 +1269,8 @@ func RegisterRoutes(m *web.Route) { | ||||
|  | ||||
| 	m.Group("/notifications", func() { | ||||
| 		m.Get("", user.Notifications) | ||||
| 		m.Get("/subscriptions", user.NotificationSubscriptions) | ||||
| 		m.Get("/watching", user.NotificationWatching) | ||||
| 		m.Post("/status", user.NotificationStatusPost) | ||||
| 		m.Post("/purge", user.NotificationPurgePost) | ||||
| 		m.Get("/new", user.NewAvailable) | ||||
|   | ||||
| @@ -171,6 +171,10 @@ | ||||
| 							{{.locale.Tr "your_starred"}} | ||||
| 						</a> | ||||
| 					{{end}} | ||||
| 					<a class="item" href="{{AppSubUrl}}/notifications/subscriptions"> | ||||
| 						{{svg "octicon-bell"}} | ||||
| 						{{.locale.Tr "notification.subscriptions"}}<!-- Subscriptions --> | ||||
| 					</a> | ||||
| 					<a class="{{if .PageIsUserSettings}}active{{end}} item" href="{{AppSubUrl}}/user/settings"> | ||||
| 						{{svg "octicon-tools"}} | ||||
| 						{{.locale.Tr "your_settings"}}<!-- Your settings --> | ||||
|   | ||||
							
								
								
									
										79
									
								
								templates/user/notification/notification_subscriptions.tmpl
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								templates/user/notification/notification_subscriptions.tmpl
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| {{template "base/head" .}} | ||||
| <div class="page-content user notification" id="notification_subscriptions" data-params="{{.Page.GetParams}}" data-sequence-number="{{.SequenceNumber}}"> | ||||
| 	<div class="ui container"> | ||||
| 		<div class="ui top attached tabular menu"> | ||||
| 			<a href="{{AppSubUrl}}/notifications/subscriptions" class="{{if eq .Status 1}}active {{end}}item"> | ||||
| 				{{.locale.Tr "notification.subscriptions"}} | ||||
| 			</a> | ||||
| 			<a href="{{AppSubUrl}}/notifications/watching" class="{{if eq .Status 2}}active {{end}}item"> | ||||
| 				{{.locale.Tr "notification.watching"}} | ||||
| 			</a> | ||||
| 		</div> | ||||
| 		<div class="ui bottom attached active tab segment"> | ||||
| 			{{if eq .Status 1}} | ||||
| 				<div id="issue-filters" class="ui stackable grid"> | ||||
| 					<div class="six wide column"> | ||||
| 						<div class="ui compact tiny menu"> | ||||
| 							<a class="{{if eq .State "all"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=all&issueType={{$.IssueType}}&labels={{$.Labels}}"> | ||||
| 								{{.locale.Tr "all"}} | ||||
| 							</a> | ||||
| 							<a class="{{if eq .State "open"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=open&issueType={{$.IssueType}}&labels={{$.Labels}}"> | ||||
| 								{{svg "octicon-issue-opened" 16 "mr-3"}} | ||||
| 								{{.locale.Tr "repo.issues.open_title"}} | ||||
| 							</a> | ||||
| 							<a class="{{if eq .State "closed"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state=closed&issueType={{$.IssueType}}&labels={{$.Labels}}"> | ||||
| 								{{svg "octicon-issue-closed" 16 "mr-3"}} | ||||
| 								{{.locale.Tr "repo.issues.closed_title"}} | ||||
| 							</a> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 					<div class="seven wide right aligned right floated column"> | ||||
| 						<div class="ui right aligned secondary filter stackable menu labels"> | ||||
| 							<!-- Type --> | ||||
| 								<div class="ui dropdown type jump item"> | ||||
| 									<span class="text"> | ||||
| 										{{.locale.Tr "repo.issues.filter_type"}} | ||||
| 										{{svg "octicon-triangle-down" 14 "dropdown icon"}} | ||||
| 									</span> | ||||
| 									<div class="menu"> | ||||
| 										<a class="{{if or (eq .IssueType "all") (not .IssueType)}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=all&labels={{$.Labels}}">{{.locale.Tr "all"}}</a> | ||||
| 										<a class="{{if eq .IssueType "issues"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=issues&labels={{$.Labels}}">{{.locale.Tr "issues"}}</a> | ||||
| 										<a class="{{if eq .IssueType "pulls"}}active {{end}}item" href="{{$.Link}}?sort={{$.SortType}}&state={{$.State}}&issueType=pulls&labels={{$.Labels}}">{{.locale.Tr "pull_requests"}}</a> | ||||
| 									</div> | ||||
| 								</div> | ||||
|  | ||||
| 							<!-- Sort --> | ||||
| 							<div class="ui dropdown type jump item"> | ||||
| 								<span class="text"> | ||||
| 									{{.locale.Tr "repo.issues.filter_sort"}} | ||||
| 									{{svg "octicon-triangle-down" 14 "dropdown icon"}} | ||||
| 								</span> | ||||
| 								<div class="menu"> | ||||
| 									<a class="{{if or (eq .SortType "latest") (not .SortType)}}active {{end}}item" href="{{$.Link}}?sort=latest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.latest"}}</a> | ||||
| 									<a class="{{if eq .SortType "oldest"}}active {{end}}item" href="{{$.Link}}?sort=oldest&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.oldest"}}</a> | ||||
| 									<a class="{{if eq .SortType "recentupdate"}}active {{end}}item" href="{{$.Link}}?sort=recentupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.recentupdate"}}</a> | ||||
| 									<a class="{{if eq .SortType "leastupdate"}}active {{end}}item" href="{{$.Link}}?sort=leastupdate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.leastupdate"}}</a> | ||||
| 									<a class="{{if eq .SortType "mostcomment"}}active {{end}}item" href="{{$.Link}}?sort=mostcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.mostcomment"}}</a> | ||||
| 									<a class="{{if eq .SortType "leastcomment"}}active {{end}}item" href="{{$.Link}}?sort=leastcomment&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.leastcomment"}}</a> | ||||
| 									<a class="{{if eq .SortType "nearduedate"}}active {{end}}item" href="{{$.Link}}?sort=nearduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.nearduedate"}}</a> | ||||
| 									<a class="{{if eq .SortType "farduedate"}}active {{end}}item" href="{{$.Link}}?sort=farduedate&state={{$.State}}&issueType={{$.IssueType}}&labels={{$.Labels}}">{{.locale.Tr "repo.issues.filter_sort.farduedate"}}</a> | ||||
| 								</div> | ||||
| 							</div> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				{{if eq (len .Issues) 0}} | ||||
| 					<div class="ui divider"></div> | ||||
| 					{{.locale.Tr "notification.no_subscriptions"}} | ||||
| 				{{else}} | ||||
| 					{{template "shared/issuelist" mergeinto . "listType" "dashboard"}} | ||||
| 				{{end}} | ||||
| 			{{else}} | ||||
| 				{{template "explore/repo_search" .}} | ||||
| 				{{template "explore/repo_list" .}} | ||||
| 				{{template "base/paginate" .}} | ||||
| 			{{end}} | ||||
| 		</div> | ||||
| 	</div> | ||||
| </div> | ||||
| {{template "base/footer" .}} | ||||
		Reference in New Issue
	
	Block a user