mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-07-25 10:57:37 +00:00
**Backport:** https://codeberg.org/forgejo/forgejo/pulls/10927
Continuation of #10598
Closes #7254
Implement repo granular watch selection
I adopted a lot of the frontend from #10598 while redoing the entire back-end. I propose this new model:
- Remove the WatchMode enum.
- Add the WatchSelection struct, which represents the granular selection. Notice that there is no `watching` bool. I tried very hard to keep the structure as simple and redundancy-free as possible. Therefore, people not watching a repo at all either don't have a watch record or one with an entirely unselected WatchSelection struct.
- Add the WatchSource enum. It replaces the WatchMode enum and has a single purpose: determine whether a watch was explicitly or automatically initiated.
Notice that replacing this
```go
And("`watch`.mode<>?", WatchModeDont).
```
with this is correct:
```go
And(
builder.Or(
builder.Eq{"`watch`.watch_selection_issues": true},
builder.Eq{"`watch`.watch_selection_pull_requests": true},
builder.Eq{"`watch`.watch_selection_releases": true},
),
).
```
That's because there are four modes: dont, none, auto and normal. When `<>` with dont, we look for auto and normal, because there are no records with none. Therefore, the old code looks for records that indicate watching. The code I replaced this with does so, too, just more granular.
Also notice that I've prepared a future `user preset` in a few places. See below for a little more info on that.
## Next PR
I plan to continue working on this. I want to implement a `user preset` option. The user sets that `user preset` in her settings and may use them in any repo.
<details>
- rename account settings to account and notifications
- user preset (always use this preset for newly accessible repos (according to AutoWatchOnChanges and AutoWatchNewRepos))
</details>
## Further PRs
- make api able to granular watch
- move (email) notifications to new notifications tab
Co-authored-by: 0ko <0ko@noreply.codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-authored-by: pat-s <patrick.schratz@gmail.com>
Reviewed-on: #10927
Reviewed-by: Gusted <gusted@noreply.codeberg.org>
(cherry picked from commit 8770ffc848)
107 lines
3 KiB
Go
107 lines
3 KiB
Go
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package mailer
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"slices"
|
|
|
|
access_model "forgejo.org/models/perm/access"
|
|
repo_model "forgejo.org/models/repo"
|
|
"forgejo.org/models/unit"
|
|
user_model "forgejo.org/models/user"
|
|
"forgejo.org/modules/base"
|
|
"forgejo.org/modules/log"
|
|
"forgejo.org/modules/markup"
|
|
"forgejo.org/modules/markup/markdown"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/modules/translation"
|
|
)
|
|
|
|
const (
|
|
tplNewReleaseMail base.TplName = "release"
|
|
)
|
|
|
|
// MailNewRelease send new release notify to all repo watchers.
|
|
func MailNewRelease(ctx context.Context, rel *repo_model.Release) {
|
|
if setting.MailService == nil {
|
|
// No mail service configured
|
|
return
|
|
}
|
|
|
|
watcherIDList, err := repo_model.GetSelectWatcherIDs(ctx, rel.RepoID, repo_model.WatchSelection{Issues: false, PullRequests: false, Releases: true})
|
|
if err != nil {
|
|
log.Error("GetRepoWatchersIDs(%d): %v", rel.RepoID, err)
|
|
return
|
|
}
|
|
|
|
recipients, err := user_model.GetMaileableUsersByIDs(ctx, watcherIDList, false)
|
|
if err != nil {
|
|
log.Error("user_model.GetMaileableUsersByIDs: %v", err)
|
|
return
|
|
}
|
|
|
|
// Users are not eligible to receive this mail if they are not active or
|
|
// they don't have permissions to read releases.
|
|
recipients = slices.DeleteFunc(recipients, func(u *user_model.User) bool {
|
|
return !u.IsActive || !access_model.CheckRepoUnitUser(ctx, rel.Repo, u, unit.TypeReleases)
|
|
})
|
|
|
|
langMap := make(map[string][]*user_model.User)
|
|
for _, user := range recipients {
|
|
if user.ID != rel.PublisherID {
|
|
langMap[user.Language] = append(langMap[user.Language], user)
|
|
}
|
|
}
|
|
|
|
for lang, tos := range langMap {
|
|
mailNewRelease(ctx, lang, tos, rel)
|
|
}
|
|
}
|
|
|
|
func mailNewRelease(ctx context.Context, lang string, tos []*user_model.User, rel *repo_model.Release) {
|
|
locale := translation.NewLocale(lang)
|
|
|
|
var err error
|
|
rel.RenderedNote, err = markdown.RenderString(&markup.RenderContext{
|
|
Ctx: ctx,
|
|
Links: markup.Links{
|
|
Base: rel.Repo.HTMLURL(),
|
|
},
|
|
Metas: rel.Repo.ComposeMetas(ctx),
|
|
}, rel.Note)
|
|
if err != nil {
|
|
log.Error("markdown.RenderString(%d): %v", rel.RepoID, err)
|
|
return
|
|
}
|
|
|
|
subject := locale.TrString("mail.release.new.subject", rel.TagName, rel.Repo.FullName())
|
|
mailMeta := map[string]any{
|
|
"locale": locale,
|
|
"Release": rel,
|
|
"Subject": subject,
|
|
"Language": locale.Language(),
|
|
"Link": rel.HTMLURL(),
|
|
}
|
|
|
|
var mailBody bytes.Buffer
|
|
|
|
if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplNewReleaseMail), mailMeta); err != nil {
|
|
log.Error("ExecuteTemplate [%s]: %v", string(tplNewReleaseMail)+"/body", err)
|
|
return
|
|
}
|
|
|
|
msgs := make([]*Message, 0, len(tos))
|
|
publisherName := fromDisplayName(rel.Publisher)
|
|
msgID := createMessageIDForRelease(rel)
|
|
for _, to := range tos {
|
|
msg := NewMessageFrom(to.EmailTo(), publisherName, setting.MailService.FromEmail, subject, mailBody.String())
|
|
msg.Info = subject
|
|
msg.SetHeader("Message-ID", msgID)
|
|
msgs = append(msgs, msg)
|
|
}
|
|
|
|
SendAsync(msgs...)
|
|
}
|