Send mentioned notifications on edit but only when the user hasn't been mentioned previously
This commit is contained in:
parent
6243185624
commit
04e0adc55b
43
pkg/migration/20210729142940.go
Normal file
43
pkg/migration/20210729142940.go
Normal 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
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
|
@ -82,6 +82,18 @@ func (t *TaskCommentCreatedEvent) Name() string {
|
||||||
return "task.comment.created"
|
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 //
|
// Namespace Events //
|
||||||
//////////////////////
|
//////////////////////
|
||||||
|
|
|
@ -17,7 +17,9 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"code.vikunja.io/api/pkg/user"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"xorm.io/xorm"
|
||||||
|
|
||||||
"code.vikunja.io/api/pkg/db"
|
"code.vikunja.io/api/pkg/db"
|
||||||
"code.vikunja.io/api/pkg/events"
|
"code.vikunja.io/api/pkg/events"
|
||||||
|
@ -44,6 +46,7 @@ func RegisterListeners() {
|
||||||
events.RegisterListener((&ListCreatedEvent{}).Name(), &SendListCreatedNotification{})
|
events.RegisterListener((&ListCreatedEvent{}).Name(), &SendListCreatedNotification{})
|
||||||
events.RegisterListener((&TaskAssigneeCreatedEvent{}).Name(), &SubscribeAssigneeToTask{})
|
events.RegisterListener((&TaskAssigneeCreatedEvent{}).Name(), &SubscribeAssigneeToTask{})
|
||||||
events.RegisterListener((&TeamMemberAddedEvent{}).Name(), &SendTeamMemberAddedNotification{})
|
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)
|
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
|
// SendTaskCommentNotification represents a listener
|
||||||
type SendTaskCommentNotification struct {
|
type SendTaskCommentNotification struct {
|
||||||
}
|
}
|
||||||
|
@ -97,33 +142,11 @@ func (s *SendTaskCommentNotification) Handle(msg *message.Message) (err error) {
|
||||||
sess := db.NewSession()
|
sess := db.NewSession()
|
||||||
defer sess.Close()
|
defer sess.Close()
|
||||||
|
|
||||||
mentionedUsers, err := FindMentionedUsersInText(sess, event.Comment.Comment)
|
mentionedUsers, err := notifyMentionedUsers(sess, event.Task, event.Comment, event.Doer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
subscribers, err := getSubscribersForEntity(sess, SubscriptionEntityTask, event.Task.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -154,6 +177,30 @@ func (s *SendTaskCommentNotification) Handle(msg *message.Message) (err error) {
|
||||||
return
|
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
|
// SendTaskAssignedNotification represents a listener
|
||||||
type SendTaskAssignedNotification struct {
|
type SendTaskAssignedNotification struct {
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,10 @@ type TaskCommentNotification struct {
|
||||||
Mentioned bool `json:"mentioned"`
|
Mentioned bool `json:"mentioned"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *TaskCommentNotification) SubjectID() int64 {
|
||||||
|
return n.Comment.ID
|
||||||
|
}
|
||||||
|
|
||||||
// ToMail returns the mail notification for TaskCommentNotification
|
// ToMail returns the mail notification for TaskCommentNotification
|
||||||
func (n *TaskCommentNotification) ToMail() *notifications.Mail {
|
func (n *TaskCommentNotification) ToMail() *notifications.Mail {
|
||||||
|
|
||||||
|
|
|
@ -132,7 +132,21 @@ func (tc *TaskComment) Update(s *xorm.Session, a web.Auth) error {
|
||||||
if updated == 0 {
|
if updated == 0 {
|
||||||
return ErrTaskCommentDoesNotExist{ID: tc.ID}
|
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
|
// ReadOne handles getting a single comment
|
||||||
|
|
|
@ -33,6 +33,8 @@ type DatabaseNotification struct {
|
||||||
Notification interface{} `xorm:"json not null" json:"notification"`
|
Notification interface{} `xorm:"json not null" json:"notification"`
|
||||||
// The name of the notification
|
// The name of the notification
|
||||||
Name string `xorm:"varchar(250) index not null" json:"name"`
|
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.
|
// When this notification is marked as read, this will be updated with the current timestamp.
|
||||||
ReadAt time.Time `xorm:"datetime null" json:"read_at"`
|
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
|
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(¬ifications)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// CanMarkNotificationAsRead checks if a user can mark a notification as read.
|
// CanMarkNotificationAsRead checks if a user can mark a notification as read.
|
||||||
func CanMarkNotificationAsRead(s *xorm.Session, notification *DatabaseNotification, notifiableID int64) (can bool, err error) {
|
func CanMarkNotificationAsRead(s *xorm.Session, notification *DatabaseNotification, notifiableID int64) (can bool, err error) {
|
||||||
can, err = s.
|
can, err = s.
|
||||||
|
|
|
@ -29,6 +29,10 @@ type Notification interface {
|
||||||
Name() string
|
Name() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SubjectID interface {
|
||||||
|
SubjectID() int64
|
||||||
|
}
|
||||||
|
|
||||||
// Notifiable is an entity which can be notified. Usually a user.
|
// Notifiable is an entity which can be notified. Usually a user.
|
||||||
type Notifiable interface {
|
type Notifiable interface {
|
||||||
// Should return the email address this notifiable has.
|
// Should return the email address this notifiable has.
|
||||||
|
@ -82,6 +86,10 @@ func notifyDB(notifiable Notifiable, notification Notification) (err error) {
|
||||||
Name: notification.Name(),
|
Name: notification.Name(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if subject, is := notification.(SubjectID); is {
|
||||||
|
dbNotification.SubjectID = subject.SubjectID()
|
||||||
|
}
|
||||||
|
|
||||||
_, err = s.Insert(dbNotification)
|
_, err = s.Insert(dbNotification)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = s.Rollback()
|
_ = s.Rollback()
|
||||||
|
|
Loading…
Reference in New Issue
Block a user