Send mentioned notifications on edit but only when the user hasn't been mentioned previously

This commit is contained in:
kolaente 2021-07-29 14:44:57 +02:00
parent 6243185624
commit 04e0adc55b
Signed by: konrad
GPG Key ID: F40E70337AB24C9B
7 changed files with 161 additions and 24 deletions

View File

@ -0,0 +1,43 @@
// Vikunja is a to-do list application to facilitate your life.
// Copyright 2018-2021 Vikunja and contributors. All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public Licensee as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public Licensee for more details.
//
// You should have received a copy of the GNU Affero General Public Licensee
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package migration
import (
"src.techknowlogick.com/xormigrate"
"xorm.io/xorm"
)
type notifications20210729142940 struct {
SubjectID int64 `xorm:"bigint null" json:"-"`
}
func (notifications20210729142940) TableName() string {
return "notifications"
}
func init() {
migrations = append(migrations, &xormigrate.Migration{
ID: "20210729142940",
Description: "Add subject id to notification",
Migrate: func(tx *xorm.Engine) error {
return tx.Sync2(notifications20210729142940{})
},
Rollback: func(tx *xorm.Engine) error {
return nil
},
})
}

View File

@ -82,6 +82,18 @@ func (t *TaskCommentCreatedEvent) Name() string {
return "task.comment.created"
}
// TaskCommentUpdatedEvent represents a TaskCommentUpdatedEvent event
type TaskCommentUpdatedEvent struct {
Task *Task
Comment *TaskComment
Doer *user.User
}
// Name defines the name for TaskCommentUpdatedEvent
func (t *TaskCommentUpdatedEvent) Name() string {
return "task.comment.edited"
}
//////////////////////
// Namespace Events //
//////////////////////

View File

@ -17,7 +17,9 @@
package models
import (
"code.vikunja.io/api/pkg/user"
"encoding/json"
"xorm.io/xorm"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/events"
@ -44,6 +46,7 @@ func RegisterListeners() {
events.RegisterListener((&ListCreatedEvent{}).Name(), &SendListCreatedNotification{})
events.RegisterListener((&TaskAssigneeCreatedEvent{}).Name(), &SubscribeAssigneeToTask{})
events.RegisterListener((&TeamMemberAddedEvent{}).Name(), &SendTeamMemberAddedNotification{})
events.RegisterListener((&TaskCommentUpdatedEvent{}).Name(), &HandleTaskCommentEditMentions{})
}
//////
@ -77,6 +80,48 @@ func (s *DecreaseTaskCounter) Handle(msg *message.Message) (err error) {
return keyvalue.DecrBy(metrics.TaskCountKey, 1)
}
func notifyMentionedUsers(sess *xorm.Session, task *Task, comment *TaskComment, doer *user.User) (users map[int64]*user.User, err error) {
users, err = FindMentionedUsersInText(sess, comment.Comment)
if err != nil {
return
}
outer:
for _, u := range users {
can, _, err := task.CanRead(sess, u)
if err != nil {
return users, err
}
if !can {
continue
}
// Don't notify a user if they were already notified
dbn, err := notifications.GetNotificationsForEventAndUser(sess, u.ID, (&TaskCommentNotification{}).Name(), comment.ID)
if err != nil {
return users, err
}
if len(dbn) > 0 {
continue outer
}
n := &TaskCommentNotification{
Doer: doer,
Task: task,
Comment: comment,
Mentioned: true,
}
err = notifications.Notify(u, n)
if err != nil {
return users, err
}
}
return
}
// SendTaskCommentNotification represents a listener
type SendTaskCommentNotification struct {
}
@ -97,33 +142,11 @@ func (s *SendTaskCommentNotification) Handle(msg *message.Message) (err error) {
sess := db.NewSession()
defer sess.Close()
mentionedUsers, err := FindMentionedUsersInText(sess, event.Comment.Comment)
mentionedUsers, err := notifyMentionedUsers(sess, event.Task, event.Comment, event.Doer)
if err != nil {
return err
}
for _, user := range mentionedUsers {
can, _, err := event.Task.CanRead(sess, user)
if err != nil {
return err
}
if !can {
continue
}
n := &TaskCommentNotification{
Doer: event.Doer,
Task: event.Task,
Comment: event.Comment,
Mentioned: true,
}
err = notifications.Notify(user, n)
if err != nil {
return err
}
}
subscribers, err := getSubscribersForEntity(sess, SubscriptionEntityTask, event.Task.ID)
if err != nil {
return err
@ -154,6 +177,30 @@ func (s *SendTaskCommentNotification) Handle(msg *message.Message) (err error) {
return
}
// HandleTaskCommentEditMentions represents a listener
type HandleTaskCommentEditMentions struct {
}
// Name defines the name for the HandleTaskCommentEditMentions listener
func (s *HandleTaskCommentEditMentions) Name() string {
return "handle.task.comment.edit.mentions"
}
// Handle is executed when the event HandleTaskCommentEditMentions listens on is fired
func (s *HandleTaskCommentEditMentions) Handle(msg *message.Message) (err error) {
event := &TaskCommentUpdatedEvent{}
err = json.Unmarshal(msg.Payload, event)
if err != nil {
return err
}
sess := db.NewSession()
defer sess.Close()
_, err = notifyMentionedUsers(sess, event.Task, event.Comment, event.Doer)
return err
}
// SendTaskAssignedNotification represents a listener
type SendTaskAssignedNotification struct {
}

View File

@ -64,6 +64,10 @@ type TaskCommentNotification struct {
Mentioned bool `json:"mentioned"`
}
func (n *TaskCommentNotification) SubjectID() int64 {
return n.Comment.ID
}
// ToMail returns the mail notification for TaskCommentNotification
func (n *TaskCommentNotification) ToMail() *notifications.Mail {

View File

@ -132,7 +132,21 @@ func (tc *TaskComment) Update(s *xorm.Session, a web.Auth) error {
if updated == 0 {
return ErrTaskCommentDoesNotExist{ID: tc.ID}
}
return err
if err != nil {
return err
}
task, err := GetTaskSimple(s, &Task{ID: tc.TaskID})
if err != nil {
return err
}
return events.Dispatch(&TaskCommentUpdatedEvent{
Task: &task,
Comment: tc,
Doer: tc.Author,
})
}
// ReadOne handles getting a single comment

View File

@ -33,6 +33,8 @@ type DatabaseNotification struct {
Notification interface{} `xorm:"json not null" json:"notification"`
// The name of the notification
Name string `xorm:"varchar(250) index not null" json:"name"`
// The thing the notification is about. Used to check if a notification for this thing already happend or not.
SubjectID int64 `xorm:"bigint null" json:"-"`
// When this notification is marked as read, this will be updated with the current timestamp.
ReadAt time.Time `xorm:"datetime null" json:"read_at"`
@ -65,6 +67,13 @@ func GetNotificationsForUser(s *xorm.Session, notifiableID int64, limit, start i
return notifications, len(notifications), total, err
}
func GetNotificationsForEventAndUser(s *xorm.Session, notifiableID int64, event string, subjectID int64) (notifications []*DatabaseNotification, err error) {
notifications = []*DatabaseNotification{}
err = s.Where("notifiable_id = ? AND name = ? AND subject_id = ?", notifiableID, event, subjectID).
Find(&notifications)
return
}
// CanMarkNotificationAsRead checks if a user can mark a notification as read.
func CanMarkNotificationAsRead(s *xorm.Session, notification *DatabaseNotification, notifiableID int64) (can bool, err error) {
can, err = s.

View File

@ -29,6 +29,10 @@ type Notification interface {
Name() string
}
type SubjectID interface {
SubjectID() int64
}
// Notifiable is an entity which can be notified. Usually a user.
type Notifiable interface {
// Should return the email address this notifiable has.
@ -82,6 +86,10 @@ func notifyDB(notifiable Notifiable, notification Notification) (err error) {
Name: notification.Name(),
}
if subject, is := notification.(SubjectID); is {
dbNotification.SubjectID = subject.SubjectID()
}
_, err = s.Insert(dbNotification)
if err != nil {
_ = s.Rollback()