+53
appview/db/collaborators.go
+53
appview/db/collaborators.go
···
3
3
import (
4
4
"fmt"
5
5
"strings"
6
+
"time"
6
7
7
8
"tangled.org/core/appview/models"
8
9
)
···
59
60
60
61
return GetRepos(e, 0, FilterIn("at_uri", repoAts))
61
62
}
63
+
64
+
func GetCollaborators(e Execer, filters ...filter) ([]models.Collaborator, error) {
65
+
var collaborators []models.Collaborator
66
+
var conditions []string
67
+
var args []any
68
+
for _, filter := range filters {
69
+
conditions = append(conditions, filter.Condition())
70
+
args = append(args, filter.Arg()...)
71
+
}
72
+
whereClause := ""
73
+
if conditions != nil {
74
+
whereClause = " where " + strings.Join(conditions, " and ")
75
+
}
76
+
query := fmt.Sprintf(`select
77
+
id,
78
+
did,
79
+
rkey,
80
+
subject_did,
81
+
repo_at,
82
+
created
83
+
from collaborators %s`,
84
+
whereClause,
85
+
)
86
+
rows, err := e.Query(query, args...)
87
+
if err != nil {
88
+
return nil, err
89
+
}
90
+
defer rows.Close()
91
+
for rows.Next() {
92
+
var collaborator models.Collaborator
93
+
var createdAt string
94
+
if err := rows.Scan(
95
+
&collaborator.Id,
96
+
&collaborator.Did,
97
+
&collaborator.Rkey,
98
+
&collaborator.SubjectDid,
99
+
&collaborator.RepoAt,
100
+
&createdAt,
101
+
); err != nil {
102
+
return nil, err
103
+
}
104
+
collaborator.Created, err = time.Parse(time.RFC3339, createdAt)
105
+
if err != nil {
106
+
collaborator.Created = time.Now()
107
+
}
108
+
collaborators = append(collaborators, collaborator)
109
+
}
110
+
if err := rows.Err(); err != nil {
111
+
return nil, err
112
+
}
113
+
return collaborators, nil
114
+
}
-20
appview/db/issues.go
-20
appview/db/issues.go
···
247
247
return GetIssuesPaginated(e, pagination.FirstPage(), filters...)
248
248
}
249
249
250
-
func GetIssue(e Execer, repoAt syntax.ATURI, issueId int) (*models.Issue, error) {
251
-
query := `select id, owner_did, rkey, created, title, body, open from issues where repo_at = ? and issue_id = ?`
252
-
row := e.QueryRow(query, repoAt, issueId)
253
-
254
-
var issue models.Issue
255
-
var createdAt string
256
-
err := row.Scan(&issue.Id, &issue.Did, &issue.Rkey, &createdAt, &issue.Title, &issue.Body, &issue.Open)
257
-
if err != nil {
258
-
return nil, err
259
-
}
260
-
261
-
createdTime, err := time.Parse(time.RFC3339, createdAt)
262
-
if err != nil {
263
-
return nil, err
264
-
}
265
-
issue.Created = createdTime
266
-
267
-
return &issue, nil
268
-
}
269
-
270
250
func AddIssueComment(e Execer, c models.IssueComment) (int64, error) {
271
251
result, err := e.Exec(
272
252
`insert into issue_comments (
+1
appview/issues/issues.go
+1
appview/issues/issues.go
+24
appview/models/issue.go
+24
appview/models/issue.go
···
54
54
Replies []*IssueComment
55
55
}
56
56
57
+
func (it *CommentListItem) Participants() []syntax.DID {
58
+
participantSet := make(map[syntax.DID]struct{})
59
+
participants := []syntax.DID{}
60
+
61
+
addParticipant := func(did syntax.DID) {
62
+
if _, exists := participantSet[did]; !exists {
63
+
participantSet[did] = struct{}{}
64
+
participants = append(participants, did)
65
+
}
66
+
}
67
+
68
+
addParticipant(syntax.DID(it.Self.Did))
69
+
70
+
for _, c := range it.Replies {
71
+
addParticipant(syntax.DID(c.Did))
72
+
}
73
+
74
+
return participants
75
+
}
76
+
57
77
func (i *Issue) CommentList() []CommentListItem {
58
78
// Create a map to quickly find comments by their aturi
59
79
toplevel := make(map[string]*CommentListItem)
···
167
187
168
188
func (i *IssueComment) IsTopLevel() bool {
169
189
return i.ReplyTo == nil
190
+
}
191
+
192
+
func (i *IssueComment) IsReply() bool {
193
+
return i.ReplyTo != nil
170
194
}
171
195
172
196
func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) {
+26
appview/models/notifications.go
+26
appview/models/notifications.go
···
82
82
IssueClosed bool
83
83
EmailNotifications bool
84
84
}
85
+
86
+
func (prefs *NotificationPreferences) ShouldNotify(t NotificationType) bool {
87
+
switch t {
88
+
case NotificationTypeRepoStarred:
89
+
return prefs.RepoStarred
90
+
case NotificationTypeIssueCreated:
91
+
return prefs.IssueCreated
92
+
case NotificationTypeIssueCommented:
93
+
return prefs.IssueCommented
94
+
case NotificationTypeIssueClosed:
95
+
return prefs.IssueClosed
96
+
case NotificationTypePullCreated:
97
+
return prefs.PullCreated
98
+
case NotificationTypePullCommented:
99
+
return prefs.PullCommented
100
+
case NotificationTypePullMerged:
101
+
return prefs.PullMerged
102
+
case NotificationTypePullClosed:
103
+
return prefs.PullMerged // same pref for now
104
+
case NotificationTypeFollowed:
105
+
return prefs.Followed
106
+
default:
107
+
return false
108
+
}
109
+
}
110
+
85
111
func DefaultNotificationPreferences(user syntax.DID) *NotificationPreferences {
86
112
return &NotificationPreferences{
87
113
UserDid: user,
+303
-251
appview/notify/db/db.go
+303
-251
appview/notify/db/db.go
···
3
3
import (
4
4
"context"
5
5
"log"
6
+
"maps"
7
+
"slices"
6
8
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
7
10
"tangled.org/core/appview/db"
8
11
"tangled.org/core/appview/models"
9
12
"tangled.org/core/appview/notify"
···
36
39
return
37
40
}
38
41
39
-
// don't notify yourself
40
-
if repo.Did == star.StarredByDid {
41
-
return
42
-
}
43
-
44
-
// check if user wants these notifications
45
-
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
46
-
if err != nil {
47
-
log.Printf("NewStar: failed to get notification preferences for %s: %v", repo.Did, err)
48
-
return
49
-
}
50
-
if !prefs.RepoStarred {
51
-
return
52
-
}
42
+
actorDid := syntax.DID(star.StarredByDid)
43
+
recipients := []syntax.DID{syntax.DID(repo.Did)}
44
+
eventType := models.NotificationTypeRepoStarred
45
+
entityType := "repo"
46
+
entityId := star.RepoAt.String()
47
+
repoId := &repo.Id
48
+
var issueId *int64
49
+
var pullId *int64
53
50
54
-
notification := &models.Notification{
55
-
RecipientDid: repo.Did,
56
-
ActorDid: star.StarredByDid,
57
-
Type: models.NotificationTypeRepoStarred,
58
-
EntityType: "repo",
59
-
EntityId: string(star.RepoAt),
60
-
RepoId: &repo.Id,
61
-
}
62
-
err = db.CreateNotification(n.db, notification)
63
-
if err != nil {
64
-
log.Printf("NewStar: failed to create notification: %v", err)
65
-
return
66
-
}
51
+
n.notifyEvent(
52
+
actorDid,
53
+
recipients,
54
+
eventType,
55
+
entityType,
56
+
entityId,
57
+
repoId,
58
+
issueId,
59
+
pullId,
60
+
)
67
61
}
68
62
69
63
func (n *databaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {
···
71
65
}
72
66
73
67
func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) {
74
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(issue.RepoAt)))
75
-
if err != nil {
76
-
log.Printf("NewIssue: failed to get repos: %v", err)
77
-
return
78
-
}
79
68
80
-
if repo.Did == issue.Did {
81
-
return
82
-
}
83
-
84
-
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
69
+
// build the recipients list
70
+
// - owner of the repo
71
+
// - collaborators in the repo
72
+
var recipients []syntax.DID
73
+
recipients = append(recipients, syntax.DID(issue.Repo.Did))
74
+
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
85
75
if err != nil {
86
-
log.Printf("NewIssue: failed to get notification preferences for %s: %v", repo.Did, err)
76
+
log.Printf("failed to fetch collaborators: %v", err)
87
77
return
88
78
}
89
-
if !prefs.IssueCreated {
90
-
return
79
+
for _, c := range collaborators {
80
+
recipients = append(recipients, c.SubjectDid)
91
81
}
92
82
93
-
notification := &models.Notification{
94
-
RecipientDid: repo.Did,
95
-
ActorDid: issue.Did,
96
-
Type: models.NotificationTypeIssueCreated,
97
-
EntityType: "issue",
98
-
EntityId: string(issue.AtUri()),
99
-
RepoId: &repo.Id,
100
-
IssueId: &issue.Id,
101
-
}
83
+
actorDid := syntax.DID(issue.Did)
84
+
eventType := models.NotificationTypeIssueCreated
85
+
entityType := "issue"
86
+
entityId := issue.AtUri().String()
87
+
repoId := &issue.Repo.Id
88
+
issueId := &issue.Id
89
+
var pullId *int64
102
90
103
-
err = db.CreateNotification(n.db, notification)
104
-
if err != nil {
105
-
log.Printf("NewIssue: failed to create notification: %v", err)
106
-
return
107
-
}
91
+
n.notifyEvent(
92
+
actorDid,
93
+
recipients,
94
+
eventType,
95
+
entityType,
96
+
entityId,
97
+
repoId,
98
+
issueId,
99
+
pullId,
100
+
)
108
101
}
109
102
110
103
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) {
···
119
112
}
120
113
issue := issues[0]
121
114
122
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(issue.RepoAt)))
123
-
if err != nil {
124
-
log.Printf("NewIssueComment: failed to get repos: %v", err)
125
-
return
126
-
}
115
+
var recipients []syntax.DID
116
+
recipients = append(recipients, syntax.DID(issue.Repo.Did))
127
117
128
-
recipients := make(map[string]bool)
118
+
if comment.IsReply() {
119
+
// if this comment is a reply, then notify everybody in that thread
120
+
parentAtUri := *comment.ReplyTo
121
+
allThreads := issue.CommentList()
129
122
130
-
// notify issue author (if not the commenter)
131
-
if issue.Did != comment.Did {
132
-
prefs, err := db.GetNotificationPreference(n.db, issue.Did)
133
-
if err == nil && prefs.IssueCommented {
134
-
recipients[issue.Did] = true
135
-
} else if err != nil {
136
-
log.Printf("NewIssueComment: failed to get preferences for issue author %s: %v", issue.Did, err)
123
+
// find the parent thread, and add all DIDs from here to the recipient list
124
+
for _, t := range allThreads {
125
+
if t.Self.AtUri().String() == parentAtUri {
126
+
recipients = append(recipients, t.Participants()...)
127
+
}
137
128
}
129
+
} else {
130
+
// not a reply, notify just the issue author
131
+
recipients = append(recipients, syntax.DID(issue.Did))
138
132
}
139
133
140
-
// notify repo owner (if not the commenter and not already added)
141
-
if repo.Did != comment.Did && repo.Did != issue.Did {
142
-
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
143
-
if err == nil && prefs.IssueCommented {
144
-
recipients[repo.Did] = true
145
-
} else if err != nil {
146
-
log.Printf("NewIssueComment: failed to get preferences for repo owner %s: %v", repo.Did, err)
147
-
}
148
-
}
134
+
actorDid := syntax.DID(comment.Did)
135
+
eventType := models.NotificationTypeIssueCommented
136
+
entityType := "issue"
137
+
entityId := issue.AtUri().String()
138
+
repoId := &issue.Repo.Id
139
+
issueId := &issue.Id
140
+
var pullId *int64
149
141
150
-
// create notifications for all recipients
151
-
for recipientDid := range recipients {
152
-
notification := &models.Notification{
153
-
RecipientDid: recipientDid,
154
-
ActorDid: comment.Did,
155
-
Type: models.NotificationTypeIssueCommented,
156
-
EntityType: "issue",
157
-
EntityId: string(issue.AtUri()),
158
-
RepoId: &repo.Id,
159
-
IssueId: &issue.Id,
160
-
}
161
-
162
-
err = db.CreateNotification(n.db, notification)
163
-
if err != nil {
164
-
log.Printf("NewIssueComment: failed to create notification for %s: %v", recipientDid, err)
165
-
}
166
-
}
142
+
n.notifyEvent(
143
+
actorDid,
144
+
recipients,
145
+
eventType,
146
+
entityType,
147
+
entityId,
148
+
repoId,
149
+
issueId,
150
+
pullId,
151
+
)
167
152
}
168
153
169
154
func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
170
-
prefs, err := db.GetNotificationPreference(n.db, follow.SubjectDid)
171
-
if err != nil {
172
-
log.Printf("NewFollow: failed to get notification preferences for %s: %v", follow.SubjectDid, err)
173
-
return
174
-
}
175
-
if !prefs.Followed {
176
-
return
177
-
}
178
-
179
-
notification := &models.Notification{
180
-
RecipientDid: follow.SubjectDid,
181
-
ActorDid: follow.UserDid,
182
-
Type: models.NotificationTypeFollowed,
183
-
EntityType: "follow",
184
-
EntityId: follow.UserDid,
185
-
}
155
+
actorDid := syntax.DID(follow.UserDid)
156
+
recipients := []syntax.DID{syntax.DID(follow.SubjectDid)}
157
+
eventType := models.NotificationTypeFollowed
158
+
entityType := "follow"
159
+
entityId := follow.UserDid
160
+
var repoId, issueId, pullId *int64
186
161
187
-
err = db.CreateNotification(n.db, notification)
188
-
if err != nil {
189
-
log.Printf("NewFollow: failed to create notification: %v", err)
190
-
return
191
-
}
162
+
n.notifyEvent(
163
+
actorDid,
164
+
recipients,
165
+
eventType,
166
+
entityType,
167
+
entityId,
168
+
repoId,
169
+
issueId,
170
+
pullId,
171
+
)
192
172
}
193
173
194
174
func (n *databaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {
···
202
182
return
203
183
}
204
184
205
-
if repo.Did == pull.OwnerDid {
206
-
return
207
-
}
208
-
209
-
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
185
+
// build the recipients list
186
+
// - owner of the repo
187
+
// - collaborators in the repo
188
+
var recipients []syntax.DID
189
+
recipients = append(recipients, syntax.DID(repo.Did))
190
+
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
210
191
if err != nil {
211
-
log.Printf("NewPull: failed to get notification preferences for %s: %v", repo.Did, err)
192
+
log.Printf("failed to fetch collaborators: %v", err)
212
193
return
213
194
}
214
-
if !prefs.PullCreated {
215
-
return
195
+
for _, c := range collaborators {
196
+
recipients = append(recipients, c.SubjectDid)
216
197
}
217
198
218
-
notification := &models.Notification{
219
-
RecipientDid: repo.Did,
220
-
ActorDid: pull.OwnerDid,
221
-
Type: models.NotificationTypePullCreated,
222
-
EntityType: "pull",
223
-
EntityId: string(pull.RepoAt),
224
-
RepoId: &repo.Id,
225
-
PullId: func() *int64 { id := int64(pull.ID); return &id }(),
226
-
}
199
+
actorDid := syntax.DID(pull.OwnerDid)
200
+
eventType := models.NotificationTypePullCreated
201
+
entityType := "pull"
202
+
entityId := pull.PullAt().String()
203
+
repoId := &repo.Id
204
+
var issueId *int64
205
+
p := int64(pull.ID)
206
+
pullId := &p
227
207
228
-
err = db.CreateNotification(n.db, notification)
229
-
if err != nil {
230
-
log.Printf("NewPull: failed to create notification: %v", err)
231
-
return
232
-
}
208
+
n.notifyEvent(
209
+
actorDid,
210
+
recipients,
211
+
eventType,
212
+
entityType,
213
+
entityId,
214
+
repoId,
215
+
issueId,
216
+
pullId,
217
+
)
233
218
}
234
219
235
220
func (n *databaseNotifier) NewPullComment(ctx context.Context, comment *models.PullComment) {
236
-
pulls, err := db.GetPulls(n.db,
237
-
db.FilterEq("repo_at", comment.RepoAt),
238
-
db.FilterEq("pull_id", comment.PullId))
221
+
pull, err := db.GetPull(n.db,
222
+
syntax.ATURI(comment.RepoAt),
223
+
comment.PullId,
224
+
)
239
225
if err != nil {
240
226
log.Printf("NewPullComment: failed to get pulls: %v", err)
241
227
return
242
228
}
243
-
if len(pulls) == 0 {
244
-
log.Printf("NewPullComment: no pull found for %s PR %d", comment.RepoAt, comment.PullId)
245
-
return
246
-
}
247
-
pull := pulls[0]
248
229
249
230
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", comment.RepoAt))
250
231
if err != nil {
···
252
233
return
253
234
}
254
235
255
-
recipients := make(map[string]bool)
256
-
257
-
// notify pull request author (if not the commenter)
258
-
if pull.OwnerDid != comment.OwnerDid {
259
-
prefs, err := db.GetNotificationPreference(n.db, pull.OwnerDid)
260
-
if err == nil && prefs.PullCommented {
261
-
recipients[pull.OwnerDid] = true
262
-
} else if err != nil {
263
-
log.Printf("NewPullComment: failed to get preferences for pull author %s: %v", pull.OwnerDid, err)
264
-
}
236
+
// build up the recipients list:
237
+
// - repo owner
238
+
// - all pull participants
239
+
var recipients []syntax.DID
240
+
recipients = append(recipients, syntax.DID(repo.Did))
241
+
for _, p := range pull.Participants() {
242
+
recipients = append(recipients, syntax.DID(p))
265
243
}
266
244
267
-
// notify repo owner (if not the commenter and not already added)
268
-
if repo.Did != comment.OwnerDid && repo.Did != pull.OwnerDid {
269
-
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
270
-
if err == nil && prefs.PullCommented {
271
-
recipients[repo.Did] = true
272
-
} else if err != nil {
273
-
log.Printf("NewPullComment: failed to get preferences for repo owner %s: %v", repo.Did, err)
274
-
}
275
-
}
276
-
277
-
for recipientDid := range recipients {
278
-
notification := &models.Notification{
279
-
RecipientDid: recipientDid,
280
-
ActorDid: comment.OwnerDid,
281
-
Type: models.NotificationTypePullCommented,
282
-
EntityType: "pull",
283
-
EntityId: comment.RepoAt,
284
-
RepoId: &repo.Id,
285
-
PullId: func() *int64 { id := int64(pull.ID); return &id }(),
286
-
}
245
+
actorDid := syntax.DID(comment.OwnerDid)
246
+
eventType := models.NotificationTypePullCommented
247
+
entityType := "pull"
248
+
entityId := pull.PullAt().String()
249
+
repoId := &repo.Id
250
+
var issueId *int64
251
+
p := int64(pull.ID)
252
+
pullId := &p
287
253
288
-
err = db.CreateNotification(n.db, notification)
289
-
if err != nil {
290
-
log.Printf("NewPullComment: failed to create notification for %s: %v", recipientDid, err)
291
-
}
292
-
}
254
+
n.notifyEvent(
255
+
actorDid,
256
+
recipients,
257
+
eventType,
258
+
entityType,
259
+
entityId,
260
+
repoId,
261
+
issueId,
262
+
pullId,
263
+
)
293
264
}
294
265
295
266
func (n *databaseNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {
···
309
280
}
310
281
311
282
func (n *databaseNotifier) NewIssueClosed(ctx context.Context, issue *models.Issue) {
312
-
// Get repo details
313
-
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(issue.RepoAt)))
283
+
// build up the recipients list:
284
+
// - repo owner
285
+
// - repo collaborators
286
+
// - all issue participants
287
+
var recipients []syntax.DID
288
+
recipients = append(recipients, syntax.DID(issue.Repo.Did))
289
+
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", issue.Repo.RepoAt()))
314
290
if err != nil {
315
-
log.Printf("NewIssueClosed: failed to get repos: %v", err)
291
+
log.Printf("failed to fetch collaborators: %v", err)
316
292
return
317
293
}
318
-
319
-
// Don't notify yourself
320
-
if repo.Did == issue.Did {
321
-
return
294
+
for _, c := range collaborators {
295
+
recipients = append(recipients, c.SubjectDid)
322
296
}
323
-
324
-
// Check if user wants these notifications
325
-
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
326
-
if err != nil {
327
-
log.Printf("NewIssueClosed: failed to get notification preferences for %s: %v", repo.Did, err)
328
-
return
329
-
}
330
-
if !prefs.IssueClosed {
331
-
return
297
+
for _, p := range issue.Participants() {
298
+
recipients = append(recipients, syntax.DID(p))
332
299
}
333
300
334
-
notification := &models.Notification{
335
-
RecipientDid: repo.Did,
336
-
ActorDid: issue.Did,
337
-
Type: models.NotificationTypeIssueClosed,
338
-
EntityType: "issue",
339
-
EntityId: string(issue.AtUri()),
340
-
RepoId: &repo.Id,
341
-
IssueId: &issue.Id,
342
-
}
301
+
actorDid := syntax.DID(issue.Repo.Did)
302
+
eventType := models.NotificationTypeIssueClosed
303
+
entityType := "pull"
304
+
entityId := issue.AtUri().String()
305
+
repoId := &issue.Repo.Id
306
+
issueId := &issue.Id
307
+
var pullId *int64
343
308
344
-
err = db.CreateNotification(n.db, notification)
345
-
if err != nil {
346
-
log.Printf("NewIssueClosed: failed to create notification: %v", err)
347
-
return
348
-
}
309
+
n.notifyEvent(
310
+
actorDid,
311
+
recipients,
312
+
eventType,
313
+
entityType,
314
+
entityId,
315
+
repoId,
316
+
issueId,
317
+
pullId,
318
+
)
349
319
}
350
320
351
321
func (n *databaseNotifier) NewPullMerged(ctx context.Context, pull *models.Pull) {
···
356
326
return
357
327
}
358
328
359
-
// Don't notify yourself
360
-
if repo.Did == pull.OwnerDid {
361
-
return
362
-
}
363
-
364
-
// Check if user wants these notifications
365
-
prefs, err := db.GetNotificationPreference(n.db, pull.OwnerDid)
329
+
// build up the recipients list:
330
+
// - repo owner
331
+
// - all pull participants
332
+
var recipients []syntax.DID
333
+
recipients = append(recipients, syntax.DID(repo.Did))
334
+
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
366
335
if err != nil {
367
-
log.Printf("NewPullMerged: failed to get notification preferences for %s: %v", pull.OwnerDid, err)
336
+
log.Printf("failed to fetch collaborators: %v", err)
368
337
return
369
338
}
370
-
if !prefs.PullMerged {
371
-
return
339
+
for _, c := range collaborators {
340
+
recipients = append(recipients, c.SubjectDid)
341
+
}
342
+
for _, p := range pull.Participants() {
343
+
recipients = append(recipients, syntax.DID(p))
372
344
}
373
345
374
-
notification := &models.Notification{
375
-
RecipientDid: pull.OwnerDid,
376
-
ActorDid: repo.Did,
377
-
Type: models.NotificationTypePullMerged,
378
-
EntityType: "pull",
379
-
EntityId: string(pull.RepoAt),
380
-
RepoId: &repo.Id,
381
-
PullId: func() *int64 { id := int64(pull.ID); return &id }(),
382
-
}
346
+
actorDid := syntax.DID(repo.Did)
347
+
eventType := models.NotificationTypePullMerged
348
+
entityType := "pull"
349
+
entityId := pull.PullAt().String()
350
+
repoId := &repo.Id
351
+
var issueId *int64
352
+
p := int64(pull.ID)
353
+
pullId := &p
383
354
384
-
err = db.CreateNotification(n.db, notification)
385
-
if err != nil {
386
-
log.Printf("NewPullMerged: failed to create notification: %v", err)
387
-
return
388
-
}
355
+
n.notifyEvent(
356
+
actorDid,
357
+
recipients,
358
+
eventType,
359
+
entityType,
360
+
entityId,
361
+
repoId,
362
+
issueId,
363
+
pullId,
364
+
)
389
365
}
390
366
391
367
func (n *databaseNotifier) NewPullClosed(ctx context.Context, pull *models.Pull) {
392
368
// Get repo details
393
369
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
394
370
if err != nil {
395
-
log.Printf("NewPullClosed: failed to get repos: %v", err)
371
+
log.Printf("NewPullMerged: failed to get repos: %v", err)
396
372
return
397
373
}
398
374
399
-
// Don't notify yourself
400
-
if repo.Did == pull.OwnerDid {
375
+
// build up the recipients list:
376
+
// - repo owner
377
+
// - all pull participants
378
+
var recipients []syntax.DID
379
+
recipients = append(recipients, syntax.DID(repo.Did))
380
+
collaborators, err := db.GetCollaborators(n.db, db.FilterEq("repo_at", repo.RepoAt()))
381
+
if err != nil {
382
+
log.Printf("failed to fetch collaborators: %v", err)
401
383
return
384
+
}
385
+
for _, c := range collaborators {
386
+
recipients = append(recipients, c.SubjectDid)
387
+
}
388
+
for _, p := range pull.Participants() {
389
+
recipients = append(recipients, syntax.DID(p))
402
390
}
403
391
404
-
// Check if user wants these notifications - reuse pull_merged preference for now
405
-
prefs, err := db.GetNotificationPreference(n.db, pull.OwnerDid)
392
+
actorDid := syntax.DID(repo.Did)
393
+
eventType := models.NotificationTypePullClosed
394
+
entityType := "pull"
395
+
entityId := pull.PullAt().String()
396
+
repoId := &repo.Id
397
+
var issueId *int64
398
+
p := int64(pull.ID)
399
+
pullId := &p
400
+
401
+
n.notifyEvent(
402
+
actorDid,
403
+
recipients,
404
+
eventType,
405
+
entityType,
406
+
entityId,
407
+
repoId,
408
+
issueId,
409
+
pullId,
410
+
)
411
+
}
412
+
413
+
func (n *databaseNotifier) notifyEvent(
414
+
actorDid syntax.DID,
415
+
recipients []syntax.DID,
416
+
eventType models.NotificationType,
417
+
entityType string,
418
+
entityId string,
419
+
repoId *int64,
420
+
issueId *int64,
421
+
pullId *int64,
422
+
) {
423
+
recipientSet := make(map[syntax.DID]struct{})
424
+
for _, did := range recipients {
425
+
// everybody except actor themselves
426
+
if did != actorDid {
427
+
recipientSet[did] = struct{}{}
428
+
}
429
+
}
430
+
431
+
prefMap, err := db.GetNotificationPreferences(
432
+
n.db,
433
+
db.FilterIn("user_did", slices.Collect(maps.Keys(recipientSet))),
434
+
)
406
435
if err != nil {
407
-
log.Printf("NewPullClosed: failed to get notification preferences for %s: %v", pull.OwnerDid, err)
436
+
// failed to get prefs for users
408
437
return
409
438
}
410
-
if !prefs.PullMerged {
439
+
440
+
// create a transaction for bulk notification storage
441
+
tx, err := n.db.Begin()
442
+
if err != nil {
443
+
// failed to start tx
411
444
return
412
445
}
446
+
defer tx.Rollback()
413
447
414
-
notification := &models.Notification{
415
-
RecipientDid: pull.OwnerDid,
416
-
ActorDid: repo.Did,
417
-
Type: models.NotificationTypePullClosed,
418
-
EntityType: "pull",
419
-
EntityId: string(pull.RepoAt),
420
-
RepoId: &repo.Id,
421
-
PullId: func() *int64 { id := int64(pull.ID); return &id }(),
448
+
// filter based on preferences
449
+
for recipientDid := range recipientSet {
450
+
prefs, ok := prefMap[recipientDid]
451
+
if !ok {
452
+
prefs = models.DefaultNotificationPreferences(recipientDid)
453
+
}
454
+
455
+
// skip users who don’t want this type
456
+
if !prefs.ShouldNotify(eventType) {
457
+
continue
458
+
}
459
+
460
+
// create notification
461
+
notif := &models.Notification{
462
+
RecipientDid: recipientDid.String(),
463
+
ActorDid: actorDid.String(),
464
+
Type: eventType,
465
+
EntityType: entityType,
466
+
EntityId: entityId,
467
+
RepoId: repoId,
468
+
IssueId: issueId,
469
+
PullId: pullId,
470
+
}
471
+
472
+
if err := db.CreateNotification(tx, notif); err != nil {
473
+
log.Printf("notifyEvent: failed to create notification for %s: %v", recipientDid, err)
474
+
}
422
475
}
423
476
424
-
err = db.CreateNotification(n.db, notification)
425
-
if err != nil {
426
-
log.Printf("NewPullClosed: failed to create notification: %v", err)
477
+
if err := tx.Commit(); err != nil {
478
+
// failed to commit
427
479
return
428
480
}
429
481
}
+1
-1
appview/pages/templates/notifications/fragments/item.html
+1
-1
appview/pages/templates/notifications/fragments/item.html
···
22
22
{{ define "notificationIcon" }}
23
23
<div class="flex-shrink-0 max-h-full w-16 h-16 relative">
24
24
<img class="object-cover rounded-full p-2" src="{{ fullAvatar .ActorDid }}" />
25
-
<div class="absolute border-2 border-white dark:border-gray-800 bg-gray-200 dark:bg-gray-700 bottom-1 right-1 rounded-full p-2 flex items-center justify-center z-10">
25
+
<div class="absolute border-2 border-white dark:border-gray-800 bg-gray-200 dark:bg-gray-700 bottom-1 right-1 rounded-full p-1 flex items-center justify-center z-10">
26
26
{{ i .Icon "size-3 text-black dark:text-white" }}
27
27
</div>
28
28
</div>