diff --git a/pkg/models/listeners.go b/pkg/models/listeners.go index 4c456f82e..7459ea1e1 100644 --- a/pkg/models/listeners.go +++ b/pkg/models/listeners.go @@ -187,11 +187,17 @@ func (s *SendTaskCommentNotification) Handle(msg *message.Message) (err error) { sess := db.NewSession() defer sess.Close() + project, err := GetProjectSimpleByID(sess, event.Task.ProjectID) + if err != nil { + return err + } + n := &TaskCommentNotification{ Doer: event.Doer, Task: event.Task, Comment: event.Comment, Mentioned: true, + Project: project, } mentionedUsers, err := notifyMentionedUsers(sess, event.Task, event.Comment.Comment, n) if err != nil { @@ -218,6 +224,7 @@ func (s *SendTaskCommentNotification) Handle(msg *message.Message) (err error) { Doer: event.Doer, Task: event.Task, Comment: event.Comment, + Project: project, } err = notifications.Notify(subscriber.User, n, sess) if err != nil { @@ -252,11 +259,17 @@ func (s *HandleTaskCommentEditMentions) Handle(msg *message.Message) (err error) sess := db.NewSession() defer sess.Close() + project, err := GetProjectSimpleByID(sess, event.Task.ProjectID) + if err != nil { + return err + } + n := &TaskCommentNotification{ Doer: event.Doer, Task: event.Task, Comment: event.Comment, Mentioned: true, + Project: project, } _, err = notifyMentionedUsers(sess, event.Task, event.Comment.Comment, n) if err != nil { @@ -297,6 +310,11 @@ func (s *SendTaskAssignedNotification) Handle(msg *message.Message) (err error) return err } + project, err := GetProjectSimpleByID(sess, task.ProjectID) + if err != nil { + return err + } + notifiedUsers := make(map[int64]bool) for _, subscriber := range subscribers { @@ -314,6 +332,7 @@ func (s *SendTaskAssignedNotification) Handle(msg *message.Message) (err error) Task: &task, Assignee: event.Assignee, Target: subscriber.User, + Project: project, } err = notifications.Notify(subscriber.User, n, sess) if err != nil { @@ -401,10 +420,16 @@ func (s *HandleTaskCreateMentions) Handle(msg *message.Message) (err error) { sess := db.NewSession() defer sess.Close() + project, err := GetProjectSimpleByID(sess, event.Task.ProjectID) + if err != nil { + return err + } + n := &UserMentionedInTaskNotification{ - Task: event.Task, - Doer: event.Doer, - IsNew: true, + Task: event.Task, + Doer: event.Doer, + IsNew: true, + Project: project, } _, err = notifyMentionedUsers(sess, event.Task, event.Task.Description, n) if err != nil { @@ -437,10 +462,16 @@ func (s *HandleTaskUpdatedMentions) Handle(msg *message.Message) (err error) { sess := db.NewSession() defer sess.Close() + project, err := GetProjectSimpleByID(sess, event.Task.ProjectID) + if err != nil { + return err + } + n := &UserMentionedInTaskNotification{ - Task: event.Task, - Doer: event.Doer, - IsNew: false, + Task: event.Task, + Doer: event.Doer, + IsNew: false, + Project: project, } _, err = notifyMentionedUsers(sess, event.Task, event.Task.Description, n) diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go index 78266b242..3d2e595bb 100644 --- a/pkg/models/notifications.go +++ b/pkg/models/notifications.go @@ -26,11 +26,22 @@ import ( "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/i18n" + "code.vikunja.io/api/pkg/modules/avatar" "code.vikunja.io/api/pkg/notifications" "code.vikunja.io/api/pkg/user" "code.vikunja.io/api/pkg/utils" ) +// getDoerAvatarDataURI returns the avatar data URI for a user, for use in email headers. +func getDoerAvatarDataURI(doer *user.User) string { + provider := avatar.GetProvider(doer) + dataURI, err := provider.AsDataURI(doer, 20) + if err != nil { + return "" + } + return dataURI +} + // getThreadID generates a Message-ID format thread ID for a task func getThreadID(taskID int64) string { domain := "vikunja" @@ -86,6 +97,7 @@ type TaskCommentNotification struct { Task *Task `json:"task"` Comment *TaskComment `json:"comment"` Mentioned bool `json:"mentioned"` + Project *Project `json:"project"` } func (n *TaskCommentNotification) SubjectID() int64 { @@ -99,19 +111,33 @@ func (n *TaskCommentNotification) ToMail(lang string) *notifications.Mail { formattedComment := formatMentionsForEmail(s, n.Comment.Comment) mail := notifications.NewMail(). + Conversational(). From(n.Doer.GetNameAndFromEmail()). - Subject(i18n.T(lang, "notifications.task.comment.subject", n.Task.Title)) + Subject(i18n.T(lang, "notifications.task.comment.subject", n.Task.Title, n.Task.GetFullIdentifier())) + // Add header line + action := i18n.T(lang, "notifications.common.actions.left_comment", n.Doer.GetName()) if n.Mentioned { - mail. - Line(i18n.T(lang, "notifications.task.comment.mentioned_message", n.Doer.GetName())). - Subject(i18n.T(lang, "notifications.task.comment.mentioned_subject", n.Doer.GetName(), n.Task.Title)) + action = i18n.T(lang, "notifications.common.actions.mentioned_you_comment", n.Doer.GetName()) + mail.Subject(i18n.T(lang, "notifications.task.comment.mentioned_subject", n.Doer.GetName(), n.Task.Title, n.Task.GetFullIdentifier())) } + headerLine := notifications.CreateConversationalHeader( + getDoerAvatarDataURI(n.Doer), + action, + n.Task.GetFrontendURL(), + n.Project.Title, + n.Task.GetFullIdentifier(), + n.Task.Title, + ) + mail.HeaderLine(headerLine) + + // Add the actual comment content wrapped in a div for consistent spacing mail.HTML(formattedComment) return mail. - Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()) + Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()). + IncludeLinkToSettings(lang) } // ToDB returns the TaskCommentNotification notification in a format which can be saved in the db @@ -135,29 +161,41 @@ type TaskAssignedNotification struct { Task *Task `json:"task"` Assignee *user.User `json:"assignee"` Target *user.User `json:"-"` + Project *Project `json:"project"` } // ToMail returns the mail notification for TaskAssignedNotification func (n *TaskAssignedNotification) ToMail(lang string) *notifications.Mail { if n.Target.ID == n.Assignee.ID { + // Notification to the assignee return notifications.NewMail(). + From(n.Doer.GetNameAndFromEmail()). Subject(i18n.T(lang, "notifications.task.assigned.subject_to_assignee", n.Task.Title, n.Task.GetFullIdentifier())). + Greeting(i18n.T(lang, "notifications.greeting", n.Target.GetName())). Line(i18n.T(lang, "notifications.task.assigned.message_to_assignee", n.Doer.GetName(), n.Task.Title)). - Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()) + Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()). + IncludeLinkToSettings(lang) } // Check if the doer assigned the task to themselves if n.Doer.ID == n.Assignee.ID { return notifications.NewMail(). + From(n.Doer.GetNameAndFromEmail()). Subject(i18n.T(lang, "notifications.task.assigned.subject_to_others_self", n.Task.Title, n.Task.GetFullIdentifier(), n.Doer.GetName())). + Greeting(i18n.T(lang, "notifications.greeting", n.Target.GetName())). Line(i18n.T(lang, "notifications.task.assigned.message_to_others_self", n.Doer.GetName())). - Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()) + Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()). + IncludeLinkToSettings(lang) } + // Notification to others about assignment return notifications.NewMail(). + From(n.Doer.GetNameAndFromEmail()). Subject(i18n.T(lang, "notifications.task.assigned.subject_to_others", n.Task.Title, n.Task.GetFullIdentifier(), n.Assignee.GetName())). + Greeting(i18n.T(lang, "notifications.greeting", n.Target.GetName())). Line(i18n.T(lang, "notifications.task.assigned.message_to_others", n.Doer.GetName(), n.Assignee.GetName())). - Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()) + Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()). + IncludeLinkToSettings(lang) } // ToDB returns the TaskAssignedNotification notification in a format which can be saved in the db @@ -343,9 +381,10 @@ func (n *UndoneTasksOverdueNotification) Name() string { // UserMentionedInTaskNotification represents a UserMentionedInTaskNotification notification type UserMentionedInTaskNotification struct { - Doer *user.User `json:"doer"` - Task *Task `json:"task"` - IsNew bool `json:"is_new"` + Doer *user.User `json:"doer"` + Task *Task `json:"task"` + IsNew bool `json:"is_new"` + Project *Project `json:"project"` } func (n *UserMentionedInTaskNotification) SubjectID() int64 { @@ -360,19 +399,39 @@ func (n *UserMentionedInTaskNotification) ToMail(lang string) *notifications.Mai var subject string if n.IsNew { - subject = i18n.T(lang, "notifications.task.mentioned.subject_new", n.Doer.GetName(), n.Task.Title) + subject = i18n.T(lang, "notifications.task.mentioned.subject_new", n.Doer.GetName(), n.Task.Title, n.Task.GetFullIdentifier()) } else { - subject = i18n.T(lang, "notifications.task.mentioned.subject", n.Doer.GetName(), n.Task.Title) + subject = i18n.T(lang, "notifications.task.mentioned.subject", n.Doer.GetName(), n.Task.Title, n.Task.GetFullIdentifier()) } mail := notifications.NewMail(). + Conversational(). From(n.Doer.GetNameAndFromEmail()). - Subject(subject). - Line(i18n.T(lang, "notifications.task.mentioned.message", n.Doer.GetName())). - HTML(formattedDescription) + Subject(subject) + + // Add header line + action := i18n.T(lang, "notifications.common.actions.mentioned_you", n.Doer.GetName()) + if n.IsNew { + action = i18n.T(lang, "notifications.common.actions.mentioned_you_new_task", n.Doer.GetName()) + } + + headerLine := notifications.CreateConversationalHeader( + getDoerAvatarDataURI(n.Doer), + action, + n.Task.GetFrontendURL(), + n.Project.Title, + n.Task.GetFullIdentifier(), + n.Task.Title, + ) + mail.HeaderLine(headerLine) + + if formattedDescription != "" { + mail.HTML(formattedDescription) + } return mail. - Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()) + Action(i18n.T(lang, "notifications.common.actions.open_task"), n.Task.GetFrontendURL()). + IncludeLinkToSettings(lang) } // ToDB returns the UserMentionedInTaskNotification notification in a format which can be saved in the db