+53
appview/db/collaborators.go
+53
appview/db/collaborators.go
···
3
import (
4
"fmt"
5
"strings"
6
+
"time"
7
8
"tangled.org/core/appview/models"
9
)
···
60
61
return GetRepos(e, 0, FilterIn("at_uri", repoAts))
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
return GetIssuesPaginated(e, pagination.FirstPage(), filters...)
248
}
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
func AddIssueComment(e Execer, c models.IssueComment) (int64, error) {
271
result, err := e.Exec(
272
`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
Replies []*IssueComment
55
}
56
57
func (i *Issue) CommentList() []CommentListItem {
58
// Create a map to quickly find comments by their aturi
59
toplevel := make(map[string]*CommentListItem)
···
167
168
func (i *IssueComment) IsTopLevel() bool {
169
return i.ReplyTo == nil
170
}
171
172
func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) {
···
54
Replies []*IssueComment
55
}
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
+
77
func (i *Issue) CommentList() []CommentListItem {
78
// Create a map to quickly find comments by their aturi
79
toplevel := make(map[string]*CommentListItem)
···
187
188
func (i *IssueComment) IsTopLevel() bool {
189
return i.ReplyTo == nil
190
+
}
191
+
192
+
func (i *IssueComment) IsReply() bool {
193
+
return i.ReplyTo != nil
194
}
195
196
func IssueCommentFromRecord(did, rkey string, record tangled.RepoIssueComment) (*IssueComment, error) {
+26
appview/models/notifications.go
+26
appview/models/notifications.go
···
82
IssueClosed bool
83
EmailNotifications bool
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
+
111
func DefaultNotificationPreferences(user syntax.DID) *NotificationPreferences {
112
return &NotificationPreferences{
113
UserDid: user,
+303
-251
appview/notify/db/db.go
+303
-251
appview/notify/db/db.go
···
3
import (
4
"context"
5
"log"
6
7
"tangled.org/core/appview/db"
8
"tangled.org/core/appview/models"
9
"tangled.org/core/appview/notify"
···
36
return
37
}
38
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
-
}
53
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
-
}
67
}
68
69
func (n *databaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {
···
71
}
72
73
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
80
-
if repo.Did == issue.Did {
81
-
return
82
-
}
83
-
84
-
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
85
if err != nil {
86
-
log.Printf("NewIssue: failed to get notification preferences for %s: %v", repo.Did, err)
87
return
88
}
89
-
if !prefs.IssueCreated {
90
-
return
91
}
92
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
-
}
102
103
-
err = db.CreateNotification(n.db, notification)
104
-
if err != nil {
105
-
log.Printf("NewIssue: failed to create notification: %v", err)
106
-
return
107
-
}
108
}
109
110
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) {
···
119
}
120
issue := issues[0]
121
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
-
}
127
128
-
recipients := make(map[string]bool)
129
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)
137
}
138
}
139
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
-
}
149
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
-
}
167
}
168
169
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
-
}
186
187
-
err = db.CreateNotification(n.db, notification)
188
-
if err != nil {
189
-
log.Printf("NewFollow: failed to create notification: %v", err)
190
-
return
191
-
}
192
}
193
194
func (n *databaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {
···
202
return
203
}
204
205
-
if repo.Did == pull.OwnerDid {
206
-
return
207
-
}
208
-
209
-
prefs, err := db.GetNotificationPreference(n.db, repo.Did)
210
if err != nil {
211
-
log.Printf("NewPull: failed to get notification preferences for %s: %v", repo.Did, err)
212
return
213
}
214
-
if !prefs.PullCreated {
215
-
return
216
}
217
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
-
}
227
228
-
err = db.CreateNotification(n.db, notification)
229
-
if err != nil {
230
-
log.Printf("NewPull: failed to create notification: %v", err)
231
-
return
232
-
}
233
}
234
235
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))
239
if err != nil {
240
log.Printf("NewPullComment: failed to get pulls: %v", err)
241
return
242
}
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
249
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", comment.RepoAt))
250
if err != nil {
···
252
return
253
}
254
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
-
}
265
}
266
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
-
}
287
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
-
}
293
}
294
295
func (n *databaseNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {
···
309
}
310
311
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)))
314
if err != nil {
315
-
log.Printf("NewIssueClosed: failed to get repos: %v", err)
316
return
317
}
318
-
319
-
// Don't notify yourself
320
-
if repo.Did == issue.Did {
321
-
return
322
}
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
332
}
333
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
-
}
343
344
-
err = db.CreateNotification(n.db, notification)
345
-
if err != nil {
346
-
log.Printf("NewIssueClosed: failed to create notification: %v", err)
347
-
return
348
-
}
349
}
350
351
func (n *databaseNotifier) NewPullMerged(ctx context.Context, pull *models.Pull) {
···
356
return
357
}
358
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)
366
if err != nil {
367
-
log.Printf("NewPullMerged: failed to get notification preferences for %s: %v", pull.OwnerDid, err)
368
return
369
}
370
-
if !prefs.PullMerged {
371
-
return
372
}
373
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
-
}
383
384
-
err = db.CreateNotification(n.db, notification)
385
-
if err != nil {
386
-
log.Printf("NewPullMerged: failed to create notification: %v", err)
387
-
return
388
-
}
389
}
390
391
func (n *databaseNotifier) NewPullClosed(ctx context.Context, pull *models.Pull) {
392
// Get repo details
393
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
394
if err != nil {
395
-
log.Printf("NewPullClosed: failed to get repos: %v", err)
396
return
397
}
398
399
-
// Don't notify yourself
400
-
if repo.Did == pull.OwnerDid {
401
return
402
}
403
404
-
// Check if user wants these notifications - reuse pull_merged preference for now
405
-
prefs, err := db.GetNotificationPreference(n.db, pull.OwnerDid)
406
if err != nil {
407
-
log.Printf("NewPullClosed: failed to get notification preferences for %s: %v", pull.OwnerDid, err)
408
return
409
}
410
-
if !prefs.PullMerged {
411
return
412
}
413
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 }(),
422
}
423
424
-
err = db.CreateNotification(n.db, notification)
425
-
if err != nil {
426
-
log.Printf("NewPullClosed: failed to create notification: %v", err)
427
return
428
}
429
}
···
3
import (
4
"context"
5
"log"
6
+
"maps"
7
+
"slices"
8
9
+
"github.com/bluesky-social/indigo/atproto/syntax"
10
"tangled.org/core/appview/db"
11
"tangled.org/core/appview/models"
12
"tangled.org/core/appview/notify"
···
39
return
40
}
41
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
50
51
+
n.notifyEvent(
52
+
actorDid,
53
+
recipients,
54
+
eventType,
55
+
entityType,
56
+
entityId,
57
+
repoId,
58
+
issueId,
59
+
pullId,
60
+
)
61
}
62
63
func (n *databaseNotifier) DeleteStar(ctx context.Context, star *models.Star) {
···
65
}
66
67
func (n *databaseNotifier) NewIssue(ctx context.Context, issue *models.Issue) {
68
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()))
75
if err != nil {
76
+
log.Printf("failed to fetch collaborators: %w", err)
77
return
78
}
79
+
for _, c := range collaborators {
80
+
recipients = append(recipients, c.SubjectDid)
81
}
82
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
90
91
+
n.notifyEvent(
92
+
actorDid,
93
+
recipients,
94
+
eventType,
95
+
entityType,
96
+
entityId,
97
+
repoId,
98
+
issueId,
99
+
pullId,
100
+
)
101
}
102
103
func (n *databaseNotifier) NewIssueComment(ctx context.Context, comment *models.IssueComment) {
···
112
}
113
issue := issues[0]
114
115
+
var recipients []syntax.DID
116
+
recipients = append(recipients, syntax.DID(issue.Repo.Did))
117
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()
122
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
+
}
128
}
129
+
} else {
130
+
// not a reply, notify just the issue author
131
+
recipients = append(recipients, syntax.DID(issue.Did))
132
}
133
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
141
142
+
n.notifyEvent(
143
+
actorDid,
144
+
recipients,
145
+
eventType,
146
+
entityType,
147
+
entityId,
148
+
repoId,
149
+
issueId,
150
+
pullId,
151
+
)
152
}
153
154
func (n *databaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {
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
161
162
+
n.notifyEvent(
163
+
actorDid,
164
+
recipients,
165
+
eventType,
166
+
entityType,
167
+
entityId,
168
+
repoId,
169
+
issueId,
170
+
pullId,
171
+
)
172
}
173
174
func (n *databaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {
···
182
return
183
}
184
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()))
191
if err != nil {
192
+
log.Printf("failed to fetch collaborators: %w", err)
193
return
194
}
195
+
for _, c := range collaborators {
196
+
recipients = append(recipients, c.SubjectDid)
197
}
198
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
207
208
+
n.notifyEvent(
209
+
actorDid,
210
+
recipients,
211
+
eventType,
212
+
entityType,
213
+
entityId,
214
+
repoId,
215
+
issueId,
216
+
pullId,
217
+
)
218
}
219
220
func (n *databaseNotifier) NewPullComment(ctx context.Context, comment *models.PullComment) {
221
+
pull, err := db.GetPull(n.db,
222
+
syntax.ATURI(comment.RepoAt),
223
+
comment.PullId,
224
+
)
225
if err != nil {
226
log.Printf("NewPullComment: failed to get pulls: %v", err)
227
return
228
}
229
230
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", comment.RepoAt))
231
if err != nil {
···
233
return
234
}
235
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))
243
}
244
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
253
254
+
n.notifyEvent(
255
+
actorDid,
256
+
recipients,
257
+
eventType,
258
+
entityType,
259
+
entityId,
260
+
repoId,
261
+
issueId,
262
+
pullId,
263
+
)
264
}
265
266
func (n *databaseNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {
···
280
}
281
282
func (n *databaseNotifier) NewIssueClosed(ctx context.Context, issue *models.Issue) {
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()))
290
if err != nil {
291
+
log.Printf("failed to fetch collaborators: %w", err)
292
return
293
}
294
+
for _, c := range collaborators {
295
+
recipients = append(recipients, c.SubjectDid)
296
}
297
+
for _, p := range issue.Participants() {
298
+
recipients = append(recipients, syntax.DID(p))
299
}
300
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
308
309
+
n.notifyEvent(
310
+
actorDid,
311
+
recipients,
312
+
eventType,
313
+
entityType,
314
+
entityId,
315
+
repoId,
316
+
issueId,
317
+
pullId,
318
+
)
319
}
320
321
func (n *databaseNotifier) NewPullMerged(ctx context.Context, pull *models.Pull) {
···
326
return
327
}
328
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()))
335
if err != nil {
336
+
log.Printf("failed to fetch collaborators: %w", err)
337
return
338
}
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))
344
}
345
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
354
355
+
n.notifyEvent(
356
+
actorDid,
357
+
recipients,
358
+
eventType,
359
+
entityType,
360
+
entityId,
361
+
repoId,
362
+
issueId,
363
+
pullId,
364
+
)
365
}
366
367
func (n *databaseNotifier) NewPullClosed(ctx context.Context, pull *models.Pull) {
368
// Get repo details
369
repo, err := db.GetRepo(n.db, db.FilterEq("at_uri", string(pull.RepoAt)))
370
if err != nil {
371
+
log.Printf("NewPullMerged: failed to get repos: %v", err)
372
return
373
}
374
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: %w", err)
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))
390
}
391
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
+
)
435
if err != nil {
436
+
// failed to get prefs for users
437
return
438
}
439
+
440
+
// create a transaction for bulk notification storage
441
+
tx, err := n.db.Begin()
442
+
if err != nil {
443
+
// failed to start tx
444
return
445
}
446
+
defer tx.Rollback()
447
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
+
}
475
}
476
477
+
if err := tx.Commit(); err != nil {
478
+
// failed to commit
479
return
480
}
481
}
+1
-1
appview/pages/templates/notifications/fragments/item.html
+1
-1
appview/pages/templates/notifications/fragments/item.html
···
22
{{ define "notificationIcon" }}
23
<div class="flex-shrink-0 max-h-full w-16 h-16 relative">
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">
26
{{ i .Icon "size-3 text-black dark:text-white" }}
27
</div>
28
</div>
···
22
{{ define "notificationIcon" }}
23
<div class="flex-shrink-0 max-h-full w-16 h-16 relative">
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-1 flex items-center justify-center z-10">
26
{{ i .Icon "size-3 text-black dark:text-white" }}
27
</div>
28
</div>