Monorepo for Tangled tangled.org

appview/notify/db: refactor db notifier

additionaly: notifies collaborators on certain events:

- issue: creation, closing
- pull: creation, closing and merging

Signed-off-by: oppiliappan <me@oppi.li>

oppi.li 2041aace a4721e14

verified
Changed files
+408 -272
appview
db
issues
models
notify
db
pages
templates
notifications
fragments
+53
appview/db/collaborators.go
··· 3 import ( 4 "fmt" 5 "strings" 6 7 "tangled.org/core/appview/models" 8 ) ··· 59 60 return GetRepos(e, 0, FilterIn("at_uri", repoAts)) 61 }
··· 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
··· 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 (
··· 247 return GetIssuesPaginated(e, pagination.FirstPage(), filters...) 248 } 249 250 func AddIssueComment(e Execer, c models.IssueComment) (int64, error) { 251 result, err := e.Exec( 252 `insert into issue_comments (
+1
appview/issues/issues.go
··· 849 Body: r.FormValue("body"), 850 Did: user.Did, 851 Created: time.Now(), 852 } 853 854 if err := rp.validator.ValidateIssue(issue); err != nil {
··· 849 Body: r.FormValue("body"), 850 Did: user.Did, 851 Created: time.Now(), 852 + Repo: &f.Repo, 853 } 854 855 if err := rp.validator.ValidateIssue(issue); err != nil {
+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
··· 82 IssueClosed bool 83 EmailNotifications bool 84 } 85 func DefaultNotificationPreferences(user syntax.DID) *NotificationPreferences { 86 return &NotificationPreferences{ 87 UserDid: user,
··· 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
··· 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
··· 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>