Files
forge/models/issues/issue_watch.go
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

135 lines
4.3 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package issues
import (
"context"
"github.com/hanzoai/git/models/db"
repo_model "github.com/hanzoai/git/models/repo"
user_model "github.com/hanzoai/git/models/user"
"github.com/hanzoai/git/modules/timeutil"
)
// IssueWatch is connection request for receiving issue notification.
type IssueWatch struct {
ID int64 `xorm:"pk autoincr"`
UserID int64 `xorm:"UNIQUE(watch) NOT NULL"`
IssueID int64 `xorm:"UNIQUE(watch) NOT NULL"`
IsWatching bool `xorm:"NOT NULL"`
CreatedUnix timeutil.TimeStamp `xorm:"created NOT NULL"`
UpdatedUnix timeutil.TimeStamp `xorm:"updated NOT NULL"`
}
func init() {
db.RegisterModel(new(IssueWatch))
}
// IssueWatchList contains IssueWatch
type IssueWatchList []*IssueWatch
// CreateOrUpdateIssueWatch set watching for a user and issue
func CreateOrUpdateIssueWatch(ctx context.Context, userID, issueID int64, isWatching bool) error {
iw, exists, err := GetIssueWatch(ctx, userID, issueID)
if err != nil {
return err
}
if !exists {
iw = &IssueWatch{
UserID: userID,
IssueID: issueID,
IsWatching: isWatching,
}
if _, err := db.GetEngine(ctx).Insert(iw); err != nil {
return err
}
} else {
iw.IsWatching = isWatching
if _, err := db.GetEngine(ctx).ID(iw.ID).Cols("is_watching", "updated_unix").Update(iw); err != nil {
return err
}
}
return nil
}
// GetIssueWatch returns all IssueWatch objects from db by user and issue
// the current Web-UI need iw object for watchers AND explicit non-watchers
func GetIssueWatch(ctx context.Context, userID, issueID int64) (iw *IssueWatch, exists bool, err error) {
iw = new(IssueWatch)
exists, err = db.GetEngine(ctx).
Where("user_id = ?", userID).
And("issue_id = ?", issueID).
Get(iw)
return iw, exists, err
}
// CheckIssueWatch check if a user is watching an issue
// it takes participants and repo watch into account
func CheckIssueWatch(ctx context.Context, user *user_model.User, issue *Issue) (bool, error) {
iw, exist, err := GetIssueWatch(ctx, user.ID, issue.ID)
if err != nil {
return false, err
}
if exist {
return iw.IsWatching, nil
}
w, err := repo_model.GetWatch(ctx, user.ID, issue.RepoID)
if err != nil {
return false, err
}
return repo_model.IsWatchMode(w.Mode) || IsUserParticipantsOfIssue(ctx, user, issue), nil
}
// GetIssueWatchersIDs returns IDs of subscribers or explicit unsubscribers to a given issue id
// but avoids joining with `user` for performance reasons
// User permissions must be verified elsewhere if required
func GetIssueWatchersIDs(ctx context.Context, issueID int64, watching bool) ([]int64, error) {
ids := make([]int64, 0, 64)
return ids, db.GetEngine(ctx).Table("issue_watch").
Where("issue_id=?", issueID).
And("is_watching = ?", watching).
Select("user_id").
Find(&ids)
}
// GetIssueWatchers returns watchers/unwatchers of a given issue
func GetIssueWatchers(ctx context.Context, issueID int64, listOptions db.ListOptions) (IssueWatchList, error) {
sess := db.GetEngine(ctx).
Where("`issue_watch`.issue_id = ?", issueID).
And("`issue_watch`.is_watching = ?", true).
And("`user`.is_active = ?", true).
And("`user`.prohibit_login = ?", false).
Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id")
if listOptions.Page > 0 {
db.SetSessionPagination(sess, &listOptions)
watches := make([]*IssueWatch, 0, listOptions.PageSize)
return watches, sess.Find(&watches)
}
watches := make([]*IssueWatch, 0, 8)
return watches, sess.Find(&watches)
}
// CountIssueWatchers count watchers/unwatchers of a given issue
func CountIssueWatchers(ctx context.Context, issueID int64) (int64, error) {
return db.GetEngine(ctx).
Where("`issue_watch`.issue_id = ?", issueID).
And("`issue_watch`.is_watching = ?", true).
And("`user`.is_active = ?", true).
And("`user`.prohibit_login = ?", false).
Join("INNER", "`user`", "`user`.id = `issue_watch`.user_id").Count(new(IssueWatch))
}
// RemoveIssueWatchersByRepoID remove issue watchers by repoID
func RemoveIssueWatchersByRepoID(ctx context.Context, userID, repoID int64) error {
_, err := db.GetEngine(ctx).
Join("INNER", "issue", "`issue`.id = `issue_watch`.issue_id AND `issue`.repo_id = ?", repoID).
Where("`issue_watch`.user_id = ?", userID).
Delete(new(IssueWatch))
return err
}