appview/notify: notify users mentioned on PR comments #739

merged
opened by boltless.me targeting master from feat/mentions
Changed files
+59 -13
appview
notify
pages
markup
pulls
+11 -1
appview/notify/db/db.go
··· 239 ) 240 } 241 242 - func (n *databaseNotifier) NewPullComment(ctx context.Context, comment *models.PullComment) { 243 pull, err := db.GetPull(n.db, 244 syntax.ATURI(comment.RepoAt), 245 comment.PullId, ··· 283 issueId, 284 pullId, 285 ) 286 } 287 288 func (n *databaseNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {
··· 239 ) 240 } 241 242 + func (n *databaseNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 243 pull, err := db.GetPull(n.db, 244 syntax.ATURI(comment.RepoAt), 245 comment.PullId, ··· 283 issueId, 284 pullId, 285 ) 286 + n.notifyEvent( 287 + actorDid, 288 + mentions, 289 + models.NotificationTypeUserMentioned, 290 + entityType, 291 + entityId, 292 + repoId, 293 + issueId, 294 + pullId, 295 + ) 296 } 297 298 func (n *databaseNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {
+2 -2
appview/notify/merged_notifier.go
··· 82 m.fanout("NewPull", ctx, pull) 83 } 84 85 - func (m *mergedNotifier) NewPullComment(ctx context.Context, comment *models.PullComment) { 86 - m.fanout("NewPullComment", ctx, comment) 87 } 88 89 func (m *mergedNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {
··· 82 m.fanout("NewPull", ctx, pull) 83 } 84 85 + func (m *mergedNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 86 + m.fanout("NewPullComment", ctx, comment, mentions) 87 } 88 89 func (m *mergedNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {
+4 -3
appview/notify/notifier.go
··· 22 DeleteFollow(ctx context.Context, follow *models.Follow) 23 24 NewPull(ctx context.Context, pull *models.Pull) 25 - NewPullComment(ctx context.Context, comment *models.PullComment) 26 NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) 27 28 UpdateProfile(ctx context.Context, profile *models.Profile) ··· 51 func (m *BaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {} 52 func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {} 53 54 - func (m *BaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {} 55 - func (m *BaseNotifier) NewPullComment(ctx context.Context, models *models.PullComment) {} 56 func (m *BaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {} 57 58 func (m *BaseNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {}
··· 22 DeleteFollow(ctx context.Context, follow *models.Follow) 23 24 NewPull(ctx context.Context, pull *models.Pull) 25 + NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) 26 NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) 27 28 UpdateProfile(ctx context.Context, profile *models.Profile) ··· 51 func (m *BaseNotifier) NewFollow(ctx context.Context, follow *models.Follow) {} 52 func (m *BaseNotifier) DeleteFollow(ctx context.Context, follow *models.Follow) {} 53 54 + func (m *BaseNotifier) NewPull(ctx context.Context, pull *models.Pull) {} 55 + func (m *BaseNotifier) NewPullComment(ctx context.Context, models *models.PullComment, mentions []syntax.DID) { 56 + } 57 func (m *BaseNotifier) NewPullState(ctx context.Context, actor syntax.DID, pull *models.Pull) {} 58 59 func (m *BaseNotifier) UpdateProfile(ctx context.Context, profile *models.Profile) {}
+4 -3
appview/notify/posthog/notifier.go
··· 86 } 87 } 88 89 - func (n *posthogNotifier) NewPullComment(ctx context.Context, comment *models.PullComment) { 90 err := n.client.Enqueue(posthog.Capture{ 91 DistinctId: comment.OwnerDid, 92 Event: "new_pull_comment", 93 Properties: posthog.Properties{ 94 - "repo_at": comment.RepoAt, 95 - "pull_id": comment.PullId, 96 }, 97 }) 98 if err != nil {
··· 86 } 87 } 88 89 + func (n *posthogNotifier) NewPullComment(ctx context.Context, comment *models.PullComment, mentions []syntax.DID) { 90 err := n.client.Enqueue(posthog.Capture{ 91 DistinctId: comment.OwnerDid, 92 Event: "new_pull_comment", 93 Properties: posthog.Properties{ 94 + "repo_at": comment.RepoAt, 95 + "pull_id": comment.PullId, 96 + "mentions": mentions, 97 }, 98 }) 99 if err != nil {
+3 -3
appview/pages/markup/extension/atlink.go
··· 16 17 // An AtNode struct represents an AtNode 18 type AtNode struct { 19 - handle string 20 ast.BaseInline 21 } 22 ··· 59 block.Advance(m[1]) 60 node := &AtNode{} 61 node.AppendChild(node, ast.NewTextSegment(atSegment)) 62 - node.handle = string(atSegment.Value(block.Source())[1:]) 63 return node 64 } 65 ··· 88 func (r *atHtmlRenderer) renderAt(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 89 if entering { 90 w.WriteString(`<a href="/@`) 91 - w.WriteString(n.(*AtNode).handle) 92 w.WriteString(`" class="mention">`) 93 } else { 94 w.WriteString("</a>")
··· 16 17 // An AtNode struct represents an AtNode 18 type AtNode struct { 19 + Handle string 20 ast.BaseInline 21 } 22 ··· 59 block.Advance(m[1]) 60 node := &AtNode{} 61 node.AppendChild(node, ast.NewTextSegment(atSegment)) 62 + node.Handle = string(atSegment.Value(block.Source())[1:]) 63 return node 64 } 65 ··· 88 func (r *atHtmlRenderer) renderAt(w util.BufWriter, source []byte, n ast.Node, entering bool) (ast.WalkStatus, error) { 89 if entering { 90 w.WriteString(`<a href="/@`) 91 + w.WriteString(n.(*AtNode).Handle) 92 w.WriteString(`" class="mention">`) 93 } else { 94 w.WriteString("</a>")
+24
appview/pages/markup/markdown.go
··· 302 return path.Join(rctx.CurrentDir, dst) 303 } 304 305 func isAbsoluteUrl(link string) bool { 306 parsed, err := url.Parse(link) 307 if err != nil {
··· 302 return path.Join(rctx.CurrentDir, dst) 303 } 304 305 + // FindUserMentions returns Set of user handles from given markup soruce. 306 + // It doesn't guarntee unique DIDs 307 + func FindUserMentions(source string) []string { 308 + var ( 309 + mentions []string 310 + mentionsSet = make(map[string]struct{}) 311 + md = NewMarkdown() 312 + sourceBytes = []byte(source) 313 + root = md.Parser().Parse(text.NewReader(sourceBytes)) 314 + ) 315 + ast.Walk(root, func(n ast.Node, entering bool) (ast.WalkStatus, error) { 316 + if entering && n.Kind() == textension.KindAt { 317 + handle := n.(*textension.AtNode).Handle 318 + mentionsSet[handle] = struct{}{} 319 + return ast.WalkSkipChildren, nil 320 + } 321 + return ast.WalkContinue, nil 322 + }) 323 + for handle := range mentionsSet { 324 + mentions = append(mentions, handle) 325 + } 326 + return mentions 327 + } 328 + 329 func isAbsoluteUrl(link string) bool { 330 parsed, err := url.Parse(link) 331 if err != nil {
+11 -1
appview/pulls/pulls.go
··· 691 } 692 693 func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 694 user := s.oauth.GetUser(r) 695 f, err := s.repoResolver.Resolve(r) 696 if err != nil { ··· 788 return 789 } 790 791 - s.notifier.NewPullComment(r.Context(), comment) 792 793 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) 794 return
··· 691 } 692 693 func (s *Pulls) PullComment(w http.ResponseWriter, r *http.Request) { 694 + l := s.logger.With("handler", "PullComment") 695 user := s.oauth.GetUser(r) 696 f, err := s.repoResolver.Resolve(r) 697 if err != nil { ··· 789 return 790 } 791 792 + rawMentions := markup.FindUserMentions(comment.Body) 793 + idents := s.idResolver.ResolveIdents(r.Context(), rawMentions) 794 + l.Debug("parsed mentions", "raw", rawMentions, "idents", idents) 795 + var mentions []syntax.DID 796 + for _, ident := range idents { 797 + if ident != nil && !ident.Handle.IsInvalidHandle() { 798 + mentions = append(mentions, ident.DID) 799 + } 800 + } 801 + s.notifier.NewPullComment(r.Context(), comment, mentions) 802 803 s.pages.HxLocation(w, fmt.Sprintf("/%s/pulls/%d#comment-%d", f.OwnerSlashRepo(), pull.PullId, commentId)) 804 return