mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-16 03:57:35 +02:00
feat: add support for watch options
This commit is contained in:
parent
35dfc6b9e1
commit
f9f00cd598
@ -115,7 +115,7 @@ func init() {
|
|||||||
db.RegisterModel(new(Notification))
|
db.RegisterModel(new(Notification))
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateRepoTransferNotification creates notification for the user a repository was transferred to
|
// CreateRepoTransferNotification creates notification for the user a repository was transferred to
|
||||||
func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error {
|
func CreateRepoTransferNotification(ctx context.Context, doer, newOwner *user_model.User, repo *repo_model.Repository) error {
|
||||||
return db.WithTx(ctx, func(ctx context.Context) error {
|
return db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
var notify []*Notification
|
var notify []*Notification
|
||||||
|
|||||||
@ -101,7 +101,12 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
|
|||||||
}
|
}
|
||||||
toNotify.AddMultiple(issueWatches...)
|
toNotify.AddMultiple(issueWatches...)
|
||||||
if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
|
if !(issue.IsPull && issues_model.HasWorkInProgressPrefix(issue.Title)) {
|
||||||
repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID)
|
repoWatchType := repo_model.WatchIssues
|
||||||
|
if issue.IsPull {
|
||||||
|
repoWatchType = repo_model.WatchPullRequests
|
||||||
|
}
|
||||||
|
|
||||||
|
repoWatches, err := repo_model.GetRepoWatchersIDs(ctx, issue.RepoID, repoWatchType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -112,6 +117,18 @@ func createOrUpdateIssueNotifications(ctx context.Context, issueID, commentID, n
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
toNotify.AddMultiple(issueParticipants...)
|
toNotify.AddMultiple(issueParticipants...)
|
||||||
|
issueAssignees, err := issues_model.GetAssigneeIDsByIssue(ctx, issueID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
toNotify.AddMultiple(issueAssignees...)
|
||||||
|
if issue.IsPull {
|
||||||
|
issueReviewers, err := issues_model.GetPullRequestRequestedReviewerIDs(ctx, issueID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
toNotify.AddMultiple(issueReviewers...)
|
||||||
|
}
|
||||||
|
|
||||||
// don't notify user who cause notification
|
// don't notify user who cause notification
|
||||||
delete(toNotify, notificationAuthorID)
|
delete(toNotify, notificationAuthorID)
|
||||||
|
|||||||
@ -81,7 +81,14 @@ func CheckIssueWatch(ctx context.Context, user *user_model.User, issue *Issue) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return repo_model.IsWatchMode(w.Mode) || IsUserParticipantsOfIssue(ctx, user, issue), nil
|
if repo_model.IsWatchMode(w.Mode) {
|
||||||
|
if issue.IsPull {
|
||||||
|
return w.PullRequests, nil
|
||||||
|
} else {
|
||||||
|
return w.Issues, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return IsUserParticipantsOfIssue(ctx, user, issue), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id
|
// GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id
|
||||||
|
|||||||
@ -1008,3 +1008,14 @@ func GetPullRequestByMergedCommit(ctx context.Context, repoID int64, sha string)
|
|||||||
|
|
||||||
return pr, nil
|
return pr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetPullRequestRequestedReviewerIDs returns IDs of reviewers requested for the given pull request
|
||||||
|
func GetPullRequestRequestedReviewerIDs(ctx context.Context, issueID int64) ([]int64, error) {
|
||||||
|
userIDs := make([]int64, 0, 5)
|
||||||
|
return userIDs, db.GetEngine(ctx).
|
||||||
|
Table("review").
|
||||||
|
Cols("reviewer_id").
|
||||||
|
Where("issue_id=?", issueID).
|
||||||
|
Distinct("reviewer_id").
|
||||||
|
Find(&userIDs)
|
||||||
|
}
|
||||||
|
|||||||
@ -409,6 +409,7 @@ func prepareMigrationTasks() []*migration {
|
|||||||
// Gitea 1.26.0 ends at migration ID number 330 (database version 331)
|
// Gitea 1.26.0 ends at migration ID number 330 (database version 331)
|
||||||
|
|
||||||
newMigration(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel),
|
newMigration(331, "Add ActionRunAttempt model and related action fields", v1_27.AddActionRunAttemptModel),
|
||||||
|
newMigration(332, "Add watch options", v1_27.AddWatchOptions),
|
||||||
}
|
}
|
||||||
return preparedMigrations
|
return preparedMigrations
|
||||||
}
|
}
|
||||||
|
|||||||
21
models/migrations/v1_27/v332.go
Normal file
21
models/migrations/v1_27/v332.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package v1_27
|
||||||
|
|
||||||
|
import (
|
||||||
|
"xorm.io/xorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func AddWatchOptions(x *xorm.Engine) error {
|
||||||
|
type Watch struct { //revive:disable-line:exported
|
||||||
|
PullRequests bool `xorm:"NOT NULL DEFAULT true"`
|
||||||
|
Issues bool `xorm:"NOT NULL DEFAULT true"`
|
||||||
|
Releases bool `xorm:"NOT NULL DEFAULT true"`
|
||||||
|
}
|
||||||
|
_, err := x.SyncWithOptions(xorm.SyncOptions{
|
||||||
|
IgnoreConstrains: true,
|
||||||
|
IgnoreIndices: true,
|
||||||
|
}, new(Watch))
|
||||||
|
return err
|
||||||
|
}
|
||||||
@ -26,14 +26,25 @@ const (
|
|||||||
WatchModeAuto // 3
|
WatchModeAuto // 3
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type WatchType int8
|
||||||
|
|
||||||
|
const (
|
||||||
|
WatchPullRequests WatchType = iota // 0
|
||||||
|
WatchIssues // 1
|
||||||
|
WatchReleases // 2
|
||||||
|
)
|
||||||
|
|
||||||
// Watch is connection request for receiving repository notification.
|
// Watch is connection request for receiving repository notification.
|
||||||
type Watch struct {
|
type Watch struct {
|
||||||
ID int64 `xorm:"pk autoincr"`
|
ID int64 `xorm:"pk autoincr"`
|
||||||
UserID int64 `xorm:"UNIQUE(watch)"`
|
UserID int64 `xorm:"UNIQUE(watch)"`
|
||||||
RepoID int64 `xorm:"UNIQUE(watch)"`
|
RepoID int64 `xorm:"UNIQUE(watch)"`
|
||||||
Mode WatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"`
|
Mode WatchMode `xorm:"SMALLINT NOT NULL DEFAULT 1"`
|
||||||
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
|
||||||
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
|
||||||
|
PullRequests bool `xorm:"NOT NULL DEFAULT true"`
|
||||||
|
Issues bool `xorm:"NOT NULL DEFAULT true"`
|
||||||
|
Releases bool `xorm:"NOT NULL DEFAULT true"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -120,11 +131,37 @@ func WatchRepo(ctx context.Context, doer *user_model.User, repo *Repository, doW
|
|||||||
return user_model.ErrBlockedUser
|
return user_model.ErrBlockedUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch.PullRequests = true
|
||||||
|
watch.Issues = true
|
||||||
|
watch.Releases = true
|
||||||
return watchRepoMode(ctx, watch, WatchModeNormal)
|
return watchRepoMode(ctx, watch, WatchModeNormal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetWatchers returns all watchers of given repository.
|
type WatchOptions struct {
|
||||||
func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
|
PullRequests bool
|
||||||
|
Issues bool
|
||||||
|
Releases bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func WatchRepoOptions(ctx context.Context, doer *user_model.User, repo *Repository, opts WatchOptions) error {
|
||||||
|
watch := Watch{UserID: doer.ID, RepoID: repo.ID}
|
||||||
|
if _, err := db.GetEngine(ctx).Get(&watch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
watch.PullRequests = opts.PullRequests
|
||||||
|
watch.Issues = opts.Issues
|
||||||
|
watch.Releases = opts.Releases
|
||||||
|
|
||||||
|
if _, err := db.GetEngine(ctx).ID(watch.ID).Cols("pull_requests", "issues", "releases").Update(&watch); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRepoWatches returns all watches of given repository.
|
||||||
|
func GetRepoWatches(ctx context.Context, repoID int64) ([]*Watch, error) {
|
||||||
watches := make([]*Watch, 0, 10)
|
watches := make([]*Watch, 0, 10)
|
||||||
return watches, db.GetEngine(ctx).Where("`watch`.repo_id=?", repoID).
|
return watches, db.GetEngine(ctx).Where("`watch`.repo_id=?", repoID).
|
||||||
And("`watch`.mode<>?", WatchModeDont).
|
And("`watch`.mode<>?", WatchModeDont).
|
||||||
@ -137,13 +174,45 @@ func GetWatchers(ctx context.Context, repoID int64) ([]*Watch, error) {
|
|||||||
// GetRepoWatchersIDs returns IDs of watchers for a given repo ID
|
// GetRepoWatchersIDs returns IDs of watchers for a given repo ID
|
||||||
// but avoids joining with `user` for performance reasons
|
// but avoids joining with `user` for performance reasons
|
||||||
// User permissions must be verified elsewhere if required
|
// User permissions must be verified elsewhere if required
|
||||||
func GetRepoWatchersIDs(ctx context.Context, repoID int64) ([]int64, error) {
|
func GetRepoWatchersIDs(ctx context.Context, repoID int64, watchType WatchType) ([]int64, error) {
|
||||||
ids := make([]int64, 0, 64)
|
ids := make([]int64, 0, 64)
|
||||||
return ids, db.GetEngine(ctx).Table("watch").
|
sess := db.GetEngine(ctx).Table("watch").
|
||||||
Where("watch.repo_id=?", repoID).
|
Where("watch.repo_id=?", repoID).
|
||||||
|
And("watch.mode<>?", WatchModeDont)
|
||||||
|
|
||||||
|
switch watchType {
|
||||||
|
case WatchPullRequests:
|
||||||
|
sess = sess.And("watch.pull_requests=?", true)
|
||||||
|
case WatchIssues:
|
||||||
|
sess = sess.And("watch.issues=?", true)
|
||||||
|
case WatchReleases:
|
||||||
|
sess = sess.And("watch.releases=?", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids, sess.Select("user_id").Find(&ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetWatches(ctx context.Context, repos []*Repository) (map[int64]*Watch, error) {
|
||||||
|
repoIDs := make([]int64, 0, len(repos))
|
||||||
|
for i := range repos {
|
||||||
|
repoIDs = append(repoIDs, repos[i].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var watches []*Watch
|
||||||
|
err := db.GetEngine(ctx).Table("watch").
|
||||||
|
In("watch.repo_id", repoIDs).
|
||||||
And("watch.mode<>?", WatchModeDont).
|
And("watch.mode<>?", WatchModeDont).
|
||||||
Select("user_id").
|
Find(&watches)
|
||||||
Find(&ids)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
watchesByRepo := make(map[int64]*Watch, len(watches))
|
||||||
|
for _, w := range watches {
|
||||||
|
watchesByRepo[w.RepoID] = w
|
||||||
|
}
|
||||||
|
|
||||||
|
return watchesByRepo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRepoWatchers returns range of users watching given repository.
|
// GetRepoWatchers returns range of users watching given repository.
|
||||||
|
|||||||
@ -32,7 +32,7 @@ func TestGetWatchers(t *testing.T) {
|
|||||||
assert.NoError(t, unittest.PrepareTestDatabase())
|
assert.NoError(t, unittest.PrepareTestDatabase())
|
||||||
|
|
||||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||||
watches, err := repo_model.GetWatchers(t.Context(), repo.ID)
|
watches, err := repo_model.GetRepoWatches(t.Context(), repo.ID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
// One watchers are inactive, thus minus 1
|
// One watchers are inactive, thus minus 1
|
||||||
assert.Len(t, watches, repo.NumWatches-1)
|
assert.Len(t, watches, repo.NumWatches-1)
|
||||||
@ -40,7 +40,7 @@ func TestGetWatchers(t *testing.T) {
|
|||||||
assert.Equal(t, repo.ID, watch.RepoID)
|
assert.Equal(t, repo.ID, watch.RepoID)
|
||||||
}
|
}
|
||||||
|
|
||||||
watches, err = repo_model.GetWatchers(t.Context(), unittest.NonexistentID)
|
watches, err = repo_model.GetRepoWatches(t.Context(), unittest.NonexistentID)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Empty(t, watches)
|
assert.Empty(t, watches)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1155,6 +1155,10 @@
|
|||||||
"repo.star_guest_user": "Sign in to star this repository.",
|
"repo.star_guest_user": "Sign in to star this repository.",
|
||||||
"repo.unwatch": "Unwatch",
|
"repo.unwatch": "Unwatch",
|
||||||
"repo.watch": "Watch",
|
"repo.watch": "Watch",
|
||||||
|
"repo.watch.options.desc": "Select notifications you want to receive in addition to mentions and subscriptions.",
|
||||||
|
"repo.watch.options.pull_requests": "Pull Requests",
|
||||||
|
"repo.watch.options.issues": "Issues",
|
||||||
|
"repo.watch.options.releases": "Releases",
|
||||||
"repo.unstar": "Unstar",
|
"repo.unstar": "Unstar",
|
||||||
"repo.star": "Star",
|
"repo.star": "Star",
|
||||||
"repo.fork": "Fork",
|
"repo.fork": "Fork",
|
||||||
|
|||||||
@ -20,11 +20,33 @@ func ActionWatch(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
watch, err := repo_model.GetWatch(ctx, ctx.Doer.ID, ctx.Repo.Repository.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetWatch", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["Watch"] = watch
|
||||||
|
ctx.Data["IsWatchingRepo"] = repo_model.IsWatchMode(watch.Mode)
|
||||||
|
|
||||||
ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name)
|
ctx.Data["Repository"], err = repo_model.GetRepositoryByName(ctx, ctx.Repo.Repository.OwnerID, ctx.Repo.Repository.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetRepositoryByName", err)
|
ctx.ServerError("GetRepositoryByName", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.HTML(http.StatusOK, tplWatchUnwatch)
|
ctx.HTML(http.StatusOK, tplWatchUnwatch)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ActionWatchOptions(ctx *context.Context) {
|
||||||
|
err := repo_model.WatchRepoOptions(ctx, ctx.Doer, ctx.Repo.Repository, repo_model.WatchOptions{
|
||||||
|
PullRequests: ctx.FormBool("pull_requests"),
|
||||||
|
Issues: ctx.FormBool("issues"),
|
||||||
|
Releases: ctx.FormBool("releases"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
handleActionError(ctx, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.JSONOK()
|
||||||
|
}
|
||||||
|
|||||||
@ -373,6 +373,13 @@ func NotificationWatching(ctx *context.Context) {
|
|||||||
ctx.Data["Total"] = count
|
ctx.Data["Total"] = count
|
||||||
ctx.Data["Repos"] = repos
|
ctx.Data["Repos"] = repos
|
||||||
|
|
||||||
|
watches, err := repo_model.GetWatches(ctx, repos)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetWatches", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["Watches"] = watches
|
||||||
|
|
||||||
// redirect to last page if request page is more than total pages
|
// redirect to last page if request page is more than total pages
|
||||||
pager := context.NewPagination(count, setting.UI.User.RepoPagingNum, page, 5)
|
pager := context.NewPagination(count, setting.UI.User.RepoPagingNum, page, 5)
|
||||||
pager.AddParamFromRequest(ctx.Req)
|
pager.AddParamFromRequest(ctx.Req)
|
||||||
|
|||||||
@ -1728,6 +1728,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
m.Get("/search", reqUnitCodeReader, repo.Search)
|
m.Get("/search", reqUnitCodeReader, repo.Search)
|
||||||
m.Post("/action/{action:star|unstar}", reqSignIn, starsEnabled, repo.ActionStar)
|
m.Post("/action/{action:star|unstar}", reqSignIn, starsEnabled, repo.ActionStar)
|
||||||
m.Post("/action/{action:watch|unwatch}", reqSignIn, repo.ActionWatch)
|
m.Post("/action/{action:watch|unwatch}", reqSignIn, repo.ActionWatch)
|
||||||
|
m.Post("/action/watch/options", reqSignIn, repo.ActionWatchOptions)
|
||||||
m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
|
m.Post("/action/{action:accept_transfer|reject_transfer}", reqSignIn, repo.ActionTransfer)
|
||||||
}, optSignIn, context.RepoAssignment)
|
}, optSignIn, context.RepoAssignment)
|
||||||
|
|
||||||
|
|||||||
@ -651,7 +651,13 @@ func repoAssignmentPrepareTemplateData(ctx *Context, data *repoAssignmentPrepare
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ctx.IsSigned {
|
if ctx.IsSigned {
|
||||||
ctx.Data["IsWatchingRepo"] = repo_model.IsWatching(ctx, ctx.Doer.ID, repo.ID)
|
watch, err := repo_model.GetWatch(ctx, ctx.Doer.ID, repo.ID)
|
||||||
|
if err != nil {
|
||||||
|
ctx.ServerError("GetWatch", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["Watch"] = watch
|
||||||
|
ctx.Data["IsWatchingRepo"] = repo_model.IsWatchMode(watch.Mode)
|
||||||
ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, repo.ID)
|
ctx.Data["IsStaringRepo"] = repo_model.IsStaring(ctx, ctx.Doer.ID, repo.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -115,16 +115,16 @@ func NotifyWatchers(ctx context.Context, acts ...*activities_model.Action) error
|
|||||||
|
|
||||||
actUserID := acts[0].ActUserID
|
actUserID := acts[0].ActUserID
|
||||||
|
|
||||||
// Add feeds for user self and all watchers.
|
// Add feeds for user self and all watches.
|
||||||
watchers, err := repo_model.GetWatchers(ctx, repoID)
|
watches, err := repo_model.GetRepoWatches(ctx, repoID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("get watchers: %w", err)
|
return fmt.Errorf("get watchers: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
permCode := make([]bool, len(watchers))
|
permCode := make([]bool, len(watches))
|
||||||
permIssue := make([]bool, len(watchers))
|
permIssue := make([]bool, len(watches))
|
||||||
permPR := make([]bool, len(watchers))
|
permPR := make([]bool, len(watches))
|
||||||
for i, watcher := range watchers {
|
for i, watcher := range watches {
|
||||||
user, err := user_model.GetUserByID(ctx, watcher.UserID)
|
user, err := user_model.GetUserByID(ctx, watcher.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
permCode[i] = false
|
permCode[i] = false
|
||||||
@ -153,7 +153,7 @@ func NotifyWatchers(ctx context.Context, acts ...*activities_model.Action) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
act.Repo = repo
|
act.Repo = repo
|
||||||
if err := notifyWatchers(ctx, act, watchers, permCode, permIssue, permPR); err != nil {
|
if err := notifyWatchers(ctx, act, watches, permCode, permIssue, permPR); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -66,7 +66,12 @@ func mailIssueCommentToParticipants(ctx context.Context, comment *mailComment, m
|
|||||||
// =========== Repo watchers ===========
|
// =========== Repo watchers ===========
|
||||||
// Make repo watchers last, since it's likely the list with the most users
|
// Make repo watchers last, since it's likely the list with the most users
|
||||||
if !(comment.Issue.IsPull && comment.Issue.PullRequest.IsWorkInProgress(ctx) && comment.ActionType != activities_model.ActionCreatePullRequest) {
|
if !(comment.Issue.IsPull && comment.Issue.PullRequest.IsWorkInProgress(ctx) && comment.ActionType != activities_model.ActionCreatePullRequest) {
|
||||||
ids, err = repo_model.GetRepoWatchersIDs(ctx, comment.Issue.RepoID)
|
var watchType repo_model.WatchType = repo_model.WatchIssues
|
||||||
|
if comment.Issue.IsPull {
|
||||||
|
watchType = repo_model.WatchPullRequests
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, err = repo_model.GetRepoWatchersIDs(ctx, comment.Issue.RepoID, watchType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("GetRepoWatchersIDs(%d): %w", comment.Issue.RepoID, err)
|
return fmt.Errorf("GetRepoWatchersIDs(%d): %w", comment.Issue.RepoID, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -35,7 +35,7 @@ func MailNewRelease(ctx context.Context, rel *repo_model.Release) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
watcherIDList, err := repo_model.GetRepoWatchersIDs(ctx, rel.RepoID)
|
watcherIDList, err := repo_model.GetRepoWatchersIDs(ctx, rel.RepoID, repo_model.WatchReleases)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetRepoWatchersIDs(%d): %v", rel.RepoID, err)
|
log.Error("GetRepoWatchersIDs(%d): %v", rel.RepoID, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@ -139,22 +139,18 @@ func (ns *notificationService) NewPullRequest(ctx context.Context, pr *issues_mo
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
toNotify := make(container.Set[int64], 32)
|
toNotify := make(container.Set[int64], 32)
|
||||||
repoWatchers, err := repo_model.GetRepoWatchersIDs(ctx, pr.Issue.RepoID)
|
repoWatchers, err := repo_model.GetRepoWatchersIDs(ctx, pr.Issue.RepoID, repo_model.WatchPullRequests)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetRepoWatchersIDs: %v", err)
|
log.Error("GetRepoWatchersIDs: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, id := range repoWatchers {
|
toNotify.AddMultiple(repoWatchers...)
|
||||||
toNotify.Add(id)
|
|
||||||
}
|
|
||||||
issueParticipants, err := issues_model.GetParticipantsIDsByIssueID(ctx, pr.IssueID)
|
issueParticipants, err := issues_model.GetParticipantsIDsByIssueID(ctx, pr.IssueID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("GetParticipantsIDsByIssueID: %v", err)
|
log.Error("GetParticipantsIDsByIssueID: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, id := range issueParticipants {
|
toNotify.AddMultiple(issueParticipants...)
|
||||||
toNotify.Add(id)
|
|
||||||
}
|
|
||||||
delete(toNotify, pr.Issue.PosterID)
|
delete(toNotify, pr.Issue.PosterID)
|
||||||
for _, mention := range mentions {
|
for _, mention := range mentions {
|
||||||
toNotify.Add(mention.ID)
|
toNotify.Add(mention.ID)
|
||||||
|
|||||||
@ -192,4 +192,5 @@
|
|||||||
</overflow-menu>
|
</overflow-menu>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui tabs divider"></div>
|
<div class="ui tabs divider"></div>
|
||||||
|
{{template "repo/watch_options_modal" $}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,19 +1,30 @@
|
|||||||
<div class="ui labeled button" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.watch_guest_user"}}"{{end}}>
|
<div class="ui buttons" {{if not $.IsSigned}}data-tooltip-content="{{ctx.Locale.Tr "repo.watch_guest_user"}}"{{end}}>
|
||||||
{{$buttonText := ctx.Locale.Tr "repo.watch"}}
|
{{$buttonText := ctx.Locale.Tr "repo.watch"}}
|
||||||
{{if $.IsWatchingRepo}}{{$buttonText = ctx.Locale.Tr "repo.unwatch"}}{{end}}
|
{{if $.IsWatchingRepo}}{{$buttonText = ctx.Locale.Tr "repo.unwatch"}}{{end}}
|
||||||
<a role="button" class="ui compact small basic button" aria-label="{{$buttonText}}"
|
<button type="button" class="ui basic compact small button" aria-label="{{$buttonText}}"
|
||||||
{{if $.IsSigned}}
|
{{if $.IsSigned}}
|
||||||
data-fetch-method="post"
|
data-fetch-method="post"
|
||||||
data-fetch-url="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}unwatch{{else}}watch{{end}}"
|
data-fetch-url="{{$.RepoLink}}/action/{{if $.IsWatchingRepo}}unwatch{{else}}watch{{end}}"
|
||||||
data-fetch-sync="$closest(.ui.labeled.button)"
|
data-fetch-sync="$closest(.ui.buttons)"
|
||||||
{{else}}
|
{{else}}
|
||||||
href="{{AppSubUrl}}/user/login"
|
disabled
|
||||||
{{end}}
|
{{end}}
|
||||||
>
|
>
|
||||||
{{svg "octicon-eye"}}
|
{{svg "octicon-eye"}}
|
||||||
<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
|
<span class="not-mobile" aria-hidden="true">{{$buttonText}}</span>
|
||||||
</a>
|
</button>
|
||||||
<a class="ui basic label" href="{{$.RepoLink}}/watchers">
|
<a class="ui basic compact small button watch-label" href="{{.RepoLink}}/watchers">
|
||||||
{{CountFmt .Repository.NumWatches}}
|
{{CountFmt .Repository.NumWatches}}
|
||||||
</a>
|
</a>
|
||||||
|
{{if $.IsWatchingRepo}}
|
||||||
|
<a class="ui basic compact small button watch-icon"
|
||||||
|
data-global-init="initWatchOptions"
|
||||||
|
data-repo-link="{{.RepoLink}}"
|
||||||
|
data-watch-pull-requests="{{$.Watch.PullRequests}}"
|
||||||
|
data-watch-issues="{{$.Watch.Issues}}"
|
||||||
|
data-watch-releases="{{$.Watch.Releases}}"
|
||||||
|
>
|
||||||
|
{{svg "octicon-gear"}}
|
||||||
|
</a>
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
33
templates/repo/watch_options_modal.tmpl
Normal file
33
templates/repo/watch_options_modal.tmpl
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<div class="ui small modal" id="repo-watch-options-modal">
|
||||||
|
<div class="header">{{ctx.Locale.Tr "notifications"}}</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>{{ctx.Locale.Tr "repo.watch.options.desc"}}</p>
|
||||||
|
<form class="ui form form-fetch-action" method="post">
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="pull_requests" type="checkbox"><label>{{ctx.Locale.Tr "repo.watch.options.pull_requests"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="issues" type="checkbox"><label>{{ctx.Locale.Tr "repo.watch.options.issues"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input name="releases" type="checkbox"><label>{{ctx.Locale.Tr "repo.watch.options.releases"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="actions">
|
||||||
|
<button class="ui small basic cancel button">
|
||||||
|
{{svg "octicon-x"}}
|
||||||
|
{{ctx.Locale.Tr "cancel"}}
|
||||||
|
</button>
|
||||||
|
<button class="ui primary small approve button">
|
||||||
|
{{svg "fontawesome-save"}}
|
||||||
|
{{ctx.Locale.Tr "save"}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
{{/* Template Attributes:
|
||||||
|
* showWatchOptions: whether to show the watch options button
|
||||||
|
*/}}
|
||||||
<div class="flex-divided-list items-with-main">
|
<div class="flex-divided-list items-with-main">
|
||||||
{{range .Repos}}
|
{{range .Repos}}
|
||||||
<div class="item">
|
<div class="item">
|
||||||
@ -37,7 +40,7 @@
|
|||||||
<div class="item-trailing muted-links">
|
<div class="item-trailing muted-links">
|
||||||
{{if .PrimaryLanguage}}
|
{{if .PrimaryLanguage}}
|
||||||
<a class="flex-text-inline" href="?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}{{if $.TabName}}&tab={{$.TabName}}{{end}}">
|
<a class="flex-text-inline" href="?q={{$.Keyword}}&sort={{$.SortType}}&language={{.PrimaryLanguage.Language}}{{if $.TabName}}&tab={{$.TabName}}{{end}}">
|
||||||
<i class="color-icon tw-mr-2" style="background-color: {{.PrimaryLanguage.Color}}"></i>
|
<i class="color-icon tw-mr-1" style="background-color: {{.PrimaryLanguage.Color}}"></i>
|
||||||
{{.PrimaryLanguage.Language}}
|
{{.PrimaryLanguage.Language}}
|
||||||
</a>
|
</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
@ -51,6 +54,19 @@
|
|||||||
<span class="tw-contents" aria-label="{{ctx.Locale.Tr "repo.forks"}}">{{svg "octicon-git-branch" 16}}</span>
|
<span class="tw-contents" aria-label="{{ctx.Locale.Tr "repo.forks"}}">{{svg "octicon-git-branch" 16}}</span>
|
||||||
<span {{if ge .NumForks 1000}}data-tooltip-content="{{.NumForks}}"{{end}}>{{CountFmt .NumForks}}</span>
|
<span {{if ge .NumForks 1000}}data-tooltip-content="{{.NumForks}}"{{end}}>{{CountFmt .NumForks}}</span>
|
||||||
</a>
|
</a>
|
||||||
|
{{if $.showWatchOptions}}
|
||||||
|
{{$watch := index $.Watches .ID}}
|
||||||
|
<a class="flex-text-inline tw-ml-1"
|
||||||
|
data-global-init="initWatchOptions"
|
||||||
|
data-repo-link="{{.Link}}"
|
||||||
|
data-watch-pull-requests="{{$watch.PullRequests}}"
|
||||||
|
data-watch-issues="{{$watch.Issues}}"
|
||||||
|
data-watch-releases="{{$watch.Releases}}"
|
||||||
|
>
|
||||||
|
{{svg "octicon-gear"}}
|
||||||
|
</a>
|
||||||
|
{{template "repo/watch_options_modal"}}
|
||||||
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{$description := .DescriptionHTML ctx}}
|
{{$description := .DescriptionHTML ctx}}
|
||||||
|
|||||||
@ -68,7 +68,7 @@
|
|||||||
{{end}}
|
{{end}}
|
||||||
{{else}}
|
{{else}}
|
||||||
{{template "shared/repo/search" .}}
|
{{template "shared/repo/search" .}}
|
||||||
{{template "shared/repo/list" .}}
|
{{template "shared/repo/list" dict "." . "showWatchOptions" true}}
|
||||||
{{template "base/paginate" .}}
|
{{template "base/paginate" .}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -73,6 +73,7 @@
|
|||||||
@import "./repo/clone.css";
|
@import "./repo/clone.css";
|
||||||
@import "./repo/commit-sign.css";
|
@import "./repo/commit-sign.css";
|
||||||
@import "./repo/packages.css";
|
@import "./repo/packages.css";
|
||||||
|
@import "./repo/watch.css";
|
||||||
|
|
||||||
@import "./editor/combomarkdowneditor.css";
|
@import "./editor/combomarkdowneditor.css";
|
||||||
|
|
||||||
|
|||||||
@ -400,11 +400,21 @@ It needs some tricks to tweak the left/right borders with active state */
|
|||||||
border-left: 1px solid var(--color-light-border);
|
border-left: 1px solid var(--color-light-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.buttons .button:first-child:hover,
|
||||||
|
.ui.buttons .button.tw-hidden:first-child + .button:hover {
|
||||||
|
border-left-color: var(--color-secondary-dark-2);
|
||||||
|
}
|
||||||
|
|
||||||
.ui.buttons .button:last-child,
|
.ui.buttons .button:last-child,
|
||||||
.ui.buttons .button:nth-last-child(2):has(+ .button.tw-hidden) {
|
.ui.buttons .button:nth-last-child(2):has(+ .button.tw-hidden) {
|
||||||
border-right: 1px solid var(--color-light-border);
|
border-right: 1px solid var(--color-light-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui.buttons .button:last-child:hover,
|
||||||
|
.ui.buttons .button:nth-last-child(2):has(+ .button.tw-hidden):hover {
|
||||||
|
border-right-color: var(--color-secondary-dark-2);
|
||||||
|
}
|
||||||
|
|
||||||
.ui.buttons .button.active {
|
.ui.buttons .button.active {
|
||||||
border-left: 1px solid var(--color-light-border);
|
border-left: 1px solid var(--color-light-border);
|
||||||
border-right: 1px solid var(--color-light-border);
|
border-right: 1px solid var(--color-light-border);
|
||||||
|
|||||||
9
web_src/css/repo/watch.css
Normal file
9
web_src/css/repo/watch.css
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
.ui.buttons .ui.watch-label {
|
||||||
|
font-size: 1rem !important;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui.buttons .ui.watch-icon {
|
||||||
|
padding: 0.42em;
|
||||||
|
}
|
||||||
35
web_src/js/features/repo-watch-options.ts
Normal file
35
web_src/js/features/repo-watch-options.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import {fomanticQuery} from '../modules/fomantic/base.ts';
|
||||||
|
import {registerGlobalInitFunc} from '../modules/observer.ts';
|
||||||
|
import {submitFormFetchAction} from './common-fetch-action.ts';
|
||||||
|
|
||||||
|
export function initRepoWatchOptions() {
|
||||||
|
const elModal = document.querySelector<HTMLElement>('#repo-watch-options-modal');
|
||||||
|
if (!elModal) return;
|
||||||
|
|
||||||
|
const elPullRequests = elModal.querySelector<HTMLInputElement>('[name="pull_requests"]')!;
|
||||||
|
const elIssues = elModal.querySelector<HTMLInputElement>('[name="issues"]')!;
|
||||||
|
const elReleases = elModal.querySelector<HTMLInputElement>('[name="releases"]')!;
|
||||||
|
|
||||||
|
const showModal = (btn: HTMLElement) => {
|
||||||
|
const form = elModal.querySelector<HTMLFormElement>('form')!;
|
||||||
|
elPullRequests.checked = btn.getAttribute('data-watch-pull-requests') === 'true';
|
||||||
|
elIssues.checked = btn.getAttribute('data-watch-issues') === 'true';
|
||||||
|
elReleases.checked = btn.getAttribute('data-watch-releases') === 'true';
|
||||||
|
form.action = `${btn.getAttribute('data-repo-link')!}/action/watch/options`;
|
||||||
|
|
||||||
|
fomanticQuery(elModal).modal({
|
||||||
|
autofocus: false,
|
||||||
|
onApprove() {
|
||||||
|
submitFormFetchAction(form);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
}).modal('show');
|
||||||
|
};
|
||||||
|
|
||||||
|
registerGlobalInitFunc('initWatchOptions', (el: HTMLElement) => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
showModal(el);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -66,6 +66,7 @@ import {initActionsPermissionsForm} from './features/common-actions-permissions.
|
|||||||
import {initRefIssueContextPopup} from './features/ref-issue.ts';
|
import {initRefIssueContextPopup} from './features/ref-issue.ts';
|
||||||
import {initGlobalShortcut} from './modules/shortcut.ts';
|
import {initGlobalShortcut} from './modules/shortcut.ts';
|
||||||
import {initDevtest} from './modules/devtest.ts';
|
import {initDevtest} from './modules/devtest.ts';
|
||||||
|
import {initRepoWatchOptions} from './features/repo-watch-options.ts';
|
||||||
|
|
||||||
const initStartTime = performance.now();
|
const initStartTime = performance.now();
|
||||||
const initPerformanceTracer = callInitFunctions([
|
const initPerformanceTracer = callInitFunctions([
|
||||||
@ -143,6 +144,7 @@ const initPerformanceTracer = callInitFunctions([
|
|||||||
initRepoContributors,
|
initRepoContributors,
|
||||||
initRepoCodeFrequency,
|
initRepoCodeFrequency,
|
||||||
initRepoRecentCommits,
|
initRepoRecentCommits,
|
||||||
|
initRepoWatchOptions,
|
||||||
|
|
||||||
initCommitStatuses,
|
initCommitStatuses,
|
||||||
initCaptcha,
|
initCaptcha,
|
||||||
|
|||||||
@ -34,7 +34,7 @@ export function registerGlobalSelectorFunc<T extends Element>(selector: string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// It handles the global init functions for all `<div data-global-int="initSomeElem"></div>` elements.
|
// It handles the global init functions for all `<div data-global-init="initSomeElem"></div>` elements.
|
||||||
export function registerGlobalInitFunc<T extends HTMLElement>(name: string, handler: GlobalInitFunc<T>) {
|
export function registerGlobalInitFunc<T extends HTMLElement>(name: string, handler: GlobalInitFunc<T>) {
|
||||||
globalInitFuncs[name] = handler as GlobalInitFunc<Element>;
|
globalInitFuncs[name] = handler as GlobalInitFunc<Element>;
|
||||||
// The "global init" functions are managed internally and called by callGlobalInitFunc
|
// The "global init" functions are managed internally and called by callGlobalInitFunc
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user