From d97daaa1ded834b9c9c339ebfbf12078a54b4810 Mon Sep 17 00:00:00 2001 From: Jyoti Patel <36148363+jp0707@users.noreply.github.com> Date: Mon, 10 May 2021 11:50:44 -0400 Subject: [PATCH] [GH-15906][MM-22844] Redesign message notification emails. (#17184) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Redesign message notification emails. * Fix tests and linter. * Fix tests * Fix tests * gofmt. * Fix date separator * Update html files * Remove date for message notification email. * Modify subtitle for mentions and direct and group messages. * Fix lint error * Fix DM subtitle. * Fixing translations Co-authored-by: Mattermod Co-authored-by: Jesús Espino --- app/email_batching.go | 154 +++---- app/email_batching_test.go | 52 --- app/file.go | 21 +- app/notification.go | 5 +- app/notification_email.go | 135 +++--- app/notification_email_test.go | 78 ++-- app/server.go | 72 +++ app/user.go | 41 +- i18n/en.json | 96 ++-- templates/invite_body.html | 90 +++- templates/messages_notification.html | 550 +++++++++++++++++++++++ templates/messages_notification.mjml | 25 ++ templates/partials/card.mjml | 30 +- templates/partials/style.mjml | 207 +++++---- templates/post_batched_body.html | 43 -- templates/post_batched_post_full.html | 38 -- templates/post_batched_post_generic.html | 37 -- templates/post_body_full.html | 45 -- templates/post_body_generic.html | 44 -- templates/reset_body.html | 56 ++- templates/verify_body.html | 56 ++- templates/welcome_body.html | 56 ++- 22 files changed, 1265 insertions(+), 666 deletions(-) create mode 100644 templates/messages_notification.html create mode 100644 templates/messages_notification.mjml delete mode 100644 templates/post_batched_body.html delete mode 100644 templates/post_batched_post_full.html delete mode 100644 templates/post_batched_post_generic.html delete mode 100644 templates/post_body_full.html delete mode 100644 templates/post_body_generic.html diff --git a/app/email_batching.go b/app/email_batching.go index 514805c76e1..153e85dafbb 100644 --- a/app/email_batching.go +++ b/app/email_batching.go @@ -4,9 +4,11 @@ package app import ( + "bytes" "context" "fmt" "html/template" + "io" "net/http" "strconv" "sync" @@ -201,33 +203,61 @@ func (es *EmailService) sendBatchedEmailNotification(userID string, notification translateFunc := i18n.GetUserTranslations(user.Locale) displayNameFormat := *es.srv.Config().TeamSettings.TeammateNameDisplay + siteURL := *es.srv.Config().ServiceSettings.SiteURL - var contents string - for _, notification := range notifications { - sender, err := es.srv.Store.User().Get(context.Background(), notification.post.UserId) - if err != nil { - mlog.Warn("Unable to find sender of post for batched email notification") - continue + postsData := make([]*postData, 0 /* len */, len(notifications) /* cap */) + embeddedFiles := make(map[string]io.Reader) + + emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL + if license := es.srv.License(); license != nil && *license.Features.EmailNotificationContents { + emailNotificationContentsType = *es.srv.Config().EmailSettings.EmailNotificationContentsType + } + + if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { + for i, notification := range notifications { + sender, errSender := es.srv.Store.User().Get(context.Background(), notification.post.UserId) + if errSender != nil { + mlog.Warn("Unable to find sender of post for batched email notification") + } + + channel, errCh := es.srv.Store.Channel().Get(notification.post.ChannelId, true) + if errCh != nil { + mlog.Warn("Unable to find channel of post for batched email notification") + } + + senderProfileImage, _, errProfileImage := es.srv.GetProfileImage(sender) + if errProfileImage != nil { + mlog.Warn("Unable to get the sender user profile image.", mlog.String("user_id", sender.Id), mlog.Err(errProfileImage)) + } + + senderPhoto := fmt.Sprintf("user-avatar-%d.png", i) + if senderProfileImage != nil { + embeddedFiles[senderPhoto] = bytes.NewReader(senderProfileImage) + } + + tm := time.Unix(notification.post.CreateAt/1000, 0) + timezone, _ := tm.Zone() + + t := translateFunc("api.email_batching.send_batched_email_notification.time", map[string]interface{}{ + "Hour": tm.Hour(), + "Minute": fmt.Sprintf("%02d", tm.Minute()), + "Month": translateFunc(tm.Month().String()), + "Day": tm.Day(), + "Year": tm.Year(), + "Timezone": timezone, + }) + + MessageURL := siteURL + "/" + notification.teamName + "/pl/" + notification.post.Id + + postsData = append(postsData, &postData{ + SenderPhoto: senderPhoto, + SenderName: sender.GetDisplayName(displayNameFormat), + Time: t, + ChannelName: channel.DisplayName, + Message: template.HTML(es.srv.GetMessageForNotification(notification.post, translateFunc)), + MessageURL: MessageURL, + }) } - - channel, errCh := es.srv.Store.Channel().Get(notification.post.ChannelId, true) - if errCh != nil { - mlog.Warn("Unable to find channel of post for batched email notification") - continue - } - - emailNotificationContentsType := model.EMAIL_NOTIFICATION_CONTENTS_FULL - if license := es.srv.License(); license != nil && *license.Features.EmailNotificationContents { - emailNotificationContentsType = *es.srv.Config().EmailSettings.EmailNotificationContentsType - } - - postContent, err := es.renderBatchedPost(notification, channel, sender, *es.srv.Config().ServiceSettings.SiteURL, displayNameFormat, translateFunc, user.Locale, emailNotificationContentsType) - if err != nil { - mlog.Warn("Unable to render post for batched email notification template", mlog.Err(err)) - continue - } - - contents += postContent } tm := time.Unix(notifications[0].post.CreateAt/1000, 0) @@ -239,59 +269,31 @@ func (es *EmailService) sendBatchedEmailNotification(userID string, notification "Day": tm.Day(), }) - data := es.newEmailTemplateData(user.Locale) - data.Props["SiteURL"] = *es.srv.Config().ServiceSettings.SiteURL - data.Props["Posts"] = template.HTML(contents) - data.Props["BodyText"] = translateFunc("api.email_batching.send_batched_email_notification.body_text", len(notifications)) - - body, err2 := es.srv.TemplatesContainer().RenderToString("post_batched_body", data) - if err2 != nil { - mlog.Warn("Unable build the batched email notification template", mlog.Err(err2)) - return + firstSender, err := es.srv.Store.User().Get(context.Background(), notifications[0].post.UserId) + if err != nil { + mlog.Warn("Unable to find sender of post for batched email notification") } - if nErr := es.sendNotificationMail(user.Email, subject, body); nErr != nil { + data := es.newEmailTemplateData(user.Locale) + data.Props["SiteURL"] = siteURL + data.Props["Title"] = translateFunc("api.email_batching.send_batched_email_notification.title", len(notifications)-1, map[string]interface{}{ + "SenderName": firstSender.GetDisplayName(displayNameFormat), + }) + data.Props["SubTitle"] = translateFunc("api.email_batching.send_batched_email_notification.subTitle") + data.Props["Button"] = translateFunc("api.email_batching.send_batched_email_notification.button") + data.Props["ButtonURL"] = siteURL + data.Props["Posts"] = postsData + data.Props["MessageButton"] = translateFunc("api.email_batching.send_batched_email_notification.messageButton") + data.Props["NotificationFooterTitle"] = translateFunc("app.notification.footer.title") + data.Props["NotificationFooterInfoLogin"] = translateFunc("app.notification.footer.infoLogin") + data.Props["NotificationFooterInfo"] = translateFunc("app.notification.footer.info") + + renderedPage, renderErr := es.srv.TemplatesContainer().RenderToString("messages_notification", data) + if renderErr != nil { + mlog.Error("Unable to render email", mlog.Err(renderErr)) + } + + if nErr := es.sendNotificationMail(user.Email, subject, renderedPage); nErr != nil { mlog.Warn("Unable to send batched email notification", mlog.String("email", user.Email), mlog.Err(nErr)) } } - -func (es *EmailService) renderBatchedPost(notification *batchedNotification, channel *model.Channel, sender *model.User, siteURL string, displayNameFormat string, translateFunc i18n.TranslateFunc, userLocale string, emailNotificationContentsType string) (string, error) { - // don't include message contents if email notification contents type is set to generic - var templateName = "post_batched_post_generic" - if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - templateName = "post_batched_post_full" - } - - data := es.newEmailTemplateData(userLocale) - data.Props["Button"] = translateFunc("api.email_batching.render_batched_post.go_to_post") - data.Props["PostMessage"] = es.srv.GetMessageForNotification(notification.post, translateFunc) - data.Props["PostLink"] = siteURL + "/" + notification.teamName + "/pl/" + notification.post.Id - data.Props["SenderName"] = sender.GetDisplayName(displayNameFormat) - - tm := time.Unix(notification.post.CreateAt/1000, 0) - timezone, _ := tm.Zone() - - data.Props["Date"] = translateFunc("api.email_batching.render_batched_post.date", map[string]interface{}{ - "Year": tm.Year(), - "Month": translateFunc(tm.Month().String()), - "Day": tm.Day(), - "Hour": tm.Hour(), - "Minute": fmt.Sprintf("%02d", tm.Minute()), - "Timezone": timezone, - }) - - if channel.Type == model.CHANNEL_DIRECT { - data.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.direct_message") - } else if channel.Type == model.CHANNEL_GROUP { - data.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.group_message") - } else { - // don't include channel name if email notification contents type is set to generic - if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - data.Props["ChannelName"] = channel.DisplayName - } else { - data.Props["ChannelName"] = translateFunc("api.email_batching.render_batched_post.notification") - } - } - - return es.srv.TemplatesContainer().RenderToString(templateName, data) -} diff --git a/app/email_batching_test.go b/app/email_batching_test.go index 4297238702f..eabbc1c450b 100644 --- a/app/email_batching_test.go +++ b/app/email_batching_test.go @@ -283,55 +283,3 @@ func TestCheckPendingNotificationsCantParseInterval(t *testing.T) { require.Nil(t, job.pendingNotifications[th.BasicUser.Id], "should have sent queued post") } - -/* - * Ensures that post contents are not included in notification email when email notification content type is set to generic - */ -func TestRenderBatchedPostGeneric(t *testing.T) { - th := SetupWithStoreMock(t) - defer th.TearDown() - - var post = &model.Post{} - post.Message = "This is the message" - var notification = &batchedNotification{} - notification.post = post - var channel = &model.Channel{} - channel.DisplayName = "Some Test Channel" - var sender = &model.User{} - sender.Email = "sender@test.com" - - translateFunc := func(translationID string, args ...interface{}) string { - // mock translateFunc just returns the translation id - this is good enough for our purposes - return translationID - } - - rendered, err := th.Server.EmailService.renderBatchedPost(notification, channel, sender, "http://localhost:8065", "", translateFunc, "en", model.EMAIL_NOTIFICATION_CONTENTS_GENERIC) - require.NoError(t, err) - require.NotContains(t, rendered, post.Message, "Rendered email should not contain post contents when email notification contents type is set to Generic.") -} - -/* - * Ensures that post contents included in notification email when email notification content type is set to full - */ -func TestRenderBatchedPostFull(t *testing.T) { - th := SetupWithStoreMock(t) - defer th.TearDown() - - var post = &model.Post{} - post.Message = "This is the message" - var notification = &batchedNotification{} - notification.post = post - var channel = &model.Channel{} - channel.DisplayName = "Some Test Channel" - var sender = &model.User{} - sender.Email = "sender@test.com" - - translateFunc := func(translationID string, args ...interface{}) string { - // mock translateFunc just returns the translation id - this is good enough for our purposes - return translationID - } - - rendered, err := th.Server.EmailService.renderBatchedPost(notification, channel, sender, "http://localhost:8065", "", translateFunc, "en", model.EMAIL_NOTIFICATION_CONTENTS_FULL) - require.NoError(t, err) - require.Contains(t, rendered, post.Message, "Rendered email should contain post contents when email notification contents type is set to Full.") -} diff --git a/app/file.go b/app/file.go index f7b4b051951..db9ed4c28bd 100644 --- a/app/file.go +++ b/app/file.go @@ -128,15 +128,7 @@ func (a *App) TestFileStoreConnectionWithConfig(cfg *model.FileSettings) *model. } func (a *App) ReadFile(path string) ([]byte, *model.AppError) { - backend, err := a.FileBackend() - if err != nil { - return nil, err - } - result, nErr := backend.ReadFile(path) - if nErr != nil { - return nil, model.NewAppError("ReadFile", "api.file.read_file.app_error", nil, nErr.Error(), http.StatusInternalServerError) - } - return result, nil + return a.srv.ReadFile(path) } // Caller must close the first return value @@ -202,16 +194,7 @@ func (a *App) MoveFile(oldPath, newPath string) *model.AppError { } func (a *App) WriteFile(fr io.Reader, path string) (int64, *model.AppError) { - backend, err := a.FileBackend() - if err != nil { - return 0, err - } - - result, nErr := backend.WriteFile(fr, path) - if nErr != nil { - return result, model.NewAppError("WriteFile", "api.file.write_file.app_error", nil, nErr.Error(), http.StatusInternalServerError) - } - return result, nil + return a.srv.WriteFile(fr, path) } func (a *App) AppendFile(fr io.Reader, path string) (int64, *model.AppError) { diff --git a/app/notification.go b/app/notification.go index 449e49ab4d8..758b196a4ed 100644 --- a/app/notification.go +++ b/app/notification.go @@ -254,8 +254,11 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod } if a.userAllowsEmail(profileMap[id], channelMemberNotifyPropsMap[id], post) { - err := a.sendNotificationEmail(notification, profileMap[id], team) + senderProfileImage, _, err := a.GetProfileImage(sender) if err != nil { + a.Log().Warn("Unable to get the sender user profile image.", mlog.String("user_id", sender.Id), mlog.Err(err)) + } + if err := a.sendNotificationEmail(notification, profileMap[id], team, senderProfileImage); err != nil { mlog.Warn("Unable to send notification email.", mlog.Err(err)) } } diff --git a/app/notification_email.go b/app/notification_email.go index ef9f065e6f4..f312ebd2422 100644 --- a/app/notification_email.go +++ b/app/notification_email.go @@ -4,9 +4,11 @@ package app import ( + "bytes" "fmt" "html" "html/template" + "io" "net/url" "path/filepath" "strings" @@ -18,7 +20,7 @@ import ( "github.com/pkg/errors" ) -func (a *App) sendNotificationEmail(notification *PostNotification, user *model.User, team *model.Team) error { +func (a *App) sendNotificationEmail(notification *PostNotification, user *model.User, team *model.Team, senderProfileImage []byte) error { channel := notification.Channel post := notification.Post @@ -95,14 +97,24 @@ func (a *App) sendNotificationEmail(notification *PostNotification, user *model. subjectText = getNotificationEmailSubject(user, post, translateFunc, *a.Config().TeamSettings.SiteName, team.DisplayName, useMilitaryTime) } + senderPhoto := "" + embeddedFiles := make(map[string]io.Reader) + if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL && senderProfileImage != nil { + senderPhoto = "user-avatar.png" + embeddedFiles = map[string]io.Reader{ + senderPhoto: bytes.NewReader(senderProfileImage), + } + } + landingURL := a.GetSiteURL() + "/landing#/" + team.Name - var bodyText, err = a.getNotificationEmailBody(user, post, channel, channelName, senderName, team.Name, landingURL, emailNotificationContentsType, useMilitaryTime, translateFunc) + + var bodyText, err = a.getNotificationEmailBody(user, post, channel, channelName, senderName, team.Name, landingURL, emailNotificationContentsType, useMilitaryTime, translateFunc, senderPhoto) if err != nil { return errors.Wrap(err, "unable to render the email notification template") } a.Srv().Go(func() { - if nErr := a.Srv().EmailService.sendNotificationMail(user.Email, html.UnescapeString(subjectText), bodyText); nErr != nil { + if nErr := a.Srv().EmailService.sendMailWithEmbeddedFiles(user.Email, html.UnescapeString(subjectText), bodyText, embeddedFiles); nErr != nil { mlog.Error("Error while sending the email", mlog.String("user_email", user.Email), mlog.Err(nErr)) } }) @@ -162,15 +174,33 @@ func getGroupMessageNotificationEmailSubject(user *model.User, post *model.Post, return translateFunc("app.notification.subject.group_message.generic", subjectParameters) } +type postData struct { + SenderName string + ChannelName string + Message template.HTML + MessageURL string + SenderPhoto string + PostPhoto string + Time string +} + /** * Computes the email body for notification messages */ -func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post, channel *model.Channel, channelName string, senderName string, teamName string, landingURL string, emailNotificationContentsType string, useMilitaryTime bool, translateFunc i18n.TranslateFunc) (string, error) { - // only include message contents in notification email if email notification contents type is set to full - var templateName = "post_body_generic" - data := a.Srv().EmailService.newEmailTemplateData(recipient.Locale) +func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post, channel *model.Channel, channelName string, senderName string, teamName string, landingURL string, emailNotificationContentsType string, useMilitaryTime bool, translateFunc i18n.TranslateFunc, senderPhoto string) (string, error) { + pData := postData{ + SenderName: senderName, + SenderPhoto: senderPhoto, + } + + t := getFormattedPostTime(recipient, post, useMilitaryTime, translateFunc) + messageTime := map[string]interface{}{ + "Hour": t.Hour, + "Minute": t.Minute, + "TimeZone": t.TimeZone, + } + if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - templateName = "post_body_full" postMessage := a.GetMessageForNotification(post, translateFunc) postMessage = html.EscapeString(postMessage) normalizedPostMessage, err := a.generateHyperlinkForChannels(postMessage, teamName, landingURL) @@ -178,72 +208,47 @@ func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post, mlog.Warn("Encountered error while generating hyperlink for channels", mlog.String("team_name", teamName), mlog.Err(err)) normalizedPostMessage = postMessage } - data.Props["PostMessage"] = template.HTML(normalizedPostMessage) + pData.Message = template.HTML(normalizedPostMessage) + pData.Time = translateFunc("app.notification.body.dm.time", messageTime) } + data := a.Srv().EmailService.newEmailTemplateData(recipient.Locale) data.Props["SiteURL"] = a.GetSiteURL() if teamName != "select_team" { - data.Props["TeamLink"] = landingURL + "/pl/" + post.Id + data.Props["ButtonURL"] = landingURL + "/pl/" + post.Id } else { - data.Props["TeamLink"] = landingURL - } - - t := getFormattedPostTime(recipient, post, useMilitaryTime, translateFunc) - - info := map[string]interface{}{ - "Hour": t.Hour, - "Minute": t.Minute, - "TimeZone": t.TimeZone, - "Month": t.Month, - "Day": t.Day, - } - if channel.Type == model.CHANNEL_DIRECT { - if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - data.Props["BodyText"] = translateFunc("app.notification.body.intro.direct.full") - data.Props["Info1"] = "" - info["SenderName"] = senderName - data.Props["Info2"] = translateFunc("app.notification.body.text.direct.full", info) - } else { - data.Props["BodyText"] = translateFunc("app.notification.body.intro.direct.generic", map[string]interface{}{ - "SenderName": senderName, - }) - data.Props["Info"] = translateFunc("app.notification.body.text.direct.generic", info) - } - } else if channel.Type == model.CHANNEL_GROUP { - if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - data.Props["BodyText"] = translateFunc("app.notification.body.intro.group_message.full") - data.Props["Info1"] = translateFunc("app.notification.body.text.group_message.full", - map[string]interface{}{ - "ChannelName": channelName, - }) - info["SenderName"] = senderName - data.Props["Info2"] = translateFunc("app.notification.body.text.group_message.full2", info) - } else { - data.Props["BodyText"] = translateFunc("app.notification.body.intro.group_message.generic", map[string]interface{}{ - "SenderName": senderName, - }) - data.Props["Info"] = translateFunc("app.notification.body.text.group_message.generic", info) - } - } else { - if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { - data.Props["BodyText"] = translateFunc("app.notification.body.intro.notification.full") - data.Props["Info1"] = translateFunc("app.notification.body.text.notification.full", - map[string]interface{}{ - "ChannelName": channelName, - }) - info["SenderName"] = senderName - data.Props["Info2"] = translateFunc("app.notification.body.text.notification.full2", info) - } else { - data.Props["BodyText"] = translateFunc("app.notification.body.intro.notification.generic", map[string]interface{}{ - "SenderName": senderName, - }) - data.Props["Info"] = translateFunc("app.notification.body.text.notification.generic", info) - } + data.Props["ButtonURL"] = landingURL } + data.Props["SenderName"] = senderName data.Props["Button"] = translateFunc("api.templates.post_body.button") + data.Props["NotificationFooterTitle"] = translateFunc("app.notification.footer.title") + data.Props["NotificationFooterInfoLogin"] = translateFunc("app.notification.footer.infoLogin") + data.Props["NotificationFooterInfo"] = translateFunc("app.notification.footer.info") - return a.Srv().TemplatesContainer().RenderToString(templateName, data) + if channel.Type == model.CHANNEL_DIRECT { + // Direct Messages + data.Props["Title"] = translateFunc("app.notification.body.dm.title", map[string]interface{}{"SenderName": senderName}) + data.Props["SubTitle"] = translateFunc("app.notification.body.dm.subTitle", map[string]interface{}{"SenderName": senderName}) + } else if channel.Type == model.CHANNEL_GROUP { + // Group Messages + data.Props["Title"] = translateFunc("app.notification.body.group.title", map[string]interface{}{"SenderName": senderName}) + data.Props["SubTitle"] = translateFunc("app.notification.body.group.subTitle", map[string]interface{}{"SenderName": senderName}) + } else { + // mentions + data.Props["Title"] = translateFunc("app.notification.body.mention.title", map[string]interface{}{"SenderName": senderName}) + data.Props["SubTitle"] = translateFunc("app.notification.body.mention.subTitle", map[string]interface{}{"SenderName": senderName, "ChannelName": channelName}) + pData.ChannelName = channelName + } + + // only include posts in notification email if email notification contents type is set to full + if emailNotificationContentsType == model.EMAIL_NOTIFICATION_CONTENTS_FULL { + data.Props["Posts"] = []postData{pData} + } else { + data.Props["Posts"] = []postData{} + } + + return a.Srv().TemplatesContainer().RenderToString("messages_notification", data) } type formattedPostTime struct { diff --git a/app/notification_email_test.go b/app/notification_email_test.go index bc243898996..61f0f9faa1e 100644 --- a/app/notification_email_test.go +++ b/app/notification_email_test.go @@ -8,7 +8,6 @@ import ( "fmt" "html/template" "regexp" - "strings" "testing" "time" @@ -91,11 +90,9 @@ func TestGetNotificationEmailBodyFullNotificationPublicChannel(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) - require.Contains(t, body, "You have a new notification.", fmt.Sprintf("Expected email text 'You have a new notification. Got %s", body)) - require.Contains(t, body, "Channel: "+channel.DisplayName, "Expected email text 'Channel: %s'. Got %s", channel.DisplayName, body) - require.Contains(t, body, senderName+" - ", fmt.Sprintf("Expected email text '%s - '. Got %s", senderName, body)) + require.Contains(t, body, "mentioned you in a message", fmt.Sprintf("Expected email text 'mentioned you in a message. Got %s", body)) require.Contains(t, body, post.Message, fmt.Sprintf("Expected email text '%s'. Got %s", post.Message, body)) require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } @@ -124,11 +121,9 @@ func TestGetNotificationEmailBodyFullNotificationGroupChannel(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) - require.Contains(t, body, "You have a new Group Message.", fmt.Sprintf("Expected email text 'You have a new Group Message. Got "+body)) - require.Contains(t, body, "Channel: ChannelName", fmt.Sprintf("Expected email text 'Channel: ChannelName'. Got %s", body)) - require.Contains(t, body, senderName+" - ", fmt.Sprintf("Expected email text '%s - '. Got %s", senderName, body)) + require.Contains(t, body, "sent you a new message", fmt.Sprintf("Expected email text 'sent you a new message. Got "+body)) require.Contains(t, body, post.Message, fmt.Sprintf("Expected email text '%s'. Got %s", post.Message, body)) require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } @@ -157,11 +152,9 @@ func TestGetNotificationEmailBodyFullNotificationPrivateChannel(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) - require.Contains(t, body, "You have a new notification.", fmt.Sprintf("Expected email text 'You have a new notification. Got "+body)) - require.Contains(t, body, "Channel: "+channel.DisplayName, fmt.Sprintf("Expected email text 'Channel: "+channel.DisplayName+"'. Got "+body)) - require.Contains(t, body, senderName+" - ", fmt.Sprintf("Expected email text '%s - '. Got %s", senderName, body)) + require.Contains(t, body, "mentioned you in a message", fmt.Sprintf("Expected email text 'mentioned you in a message. Got "+body)) require.Contains(t, body, post.Message, fmt.Sprintf("Expected email text '%s'. Got %s", post.Message, body)) require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } @@ -190,10 +183,9 @@ func TestGetNotificationEmailBodyFullNotificationDirectChannel(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) - require.Contains(t, body, "You have a new Direct Message.", fmt.Sprintf("Expected email text 'You have a new Direct Message. Got "+body)) - require.Contains(t, body, senderName+" - ", fmt.Sprintf("Expected email text '%s - '. Got %s", senderName, body)) + require.Contains(t, body, "sent you a new message", fmt.Sprintf("Expected email text 'sent you a new message. Got "+body)) require.Contains(t, body, post.Message, fmt.Sprintf("Expected email text '%s'. Got %s", post.Message, body)) require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } @@ -226,11 +218,11 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTimeWithTimezone(t *testi teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, false, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, false, translateFunc, "user-avatar.png") require.NoError(t, err) r, _ := regexp.Compile("E([S|D]+)T") zone := r.FindString(body) - require.Contains(t, body, "sender - 9:43 AM "+zone+", April 25", fmt.Sprintf("Expected email text 'sender - 9:43 AM %s, April 25'. Got %s", zone, body)) + require.Contains(t, body, "9:43 AM "+zone, fmt.Sprintf("Expected email text '9:43 AM %s'. Got %s", zone, body)) } func TestGetNotificationEmailBodyFullNotificationLocaleTimeNoTimezone(t *testing.T) { @@ -276,10 +268,10 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTimeNoTimezone(t *testing tmp, err := template.New("foo").Parse(`{{.}}`) require.NoError(t, err) var text bytes.Buffer - err = tmp.Execute(&text, fmt.Sprintf("sender - %s:%s %s, %s %s", formattedTime.Hour, formattedTime.Minute, formattedTime.TimeZone, formattedTime.Month, formattedTime.Day)) + err = tmp.Execute(&text, fmt.Sprintf("%s:%s %s", formattedTime.Hour, formattedTime.Minute, formattedTime.TimeZone)) require.NoError(t, err) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) postTimeLine := text.String() require.Contains(t, body, postTimeLine, fmt.Sprintf("Expected email text '%s'. Got %s", postTimeLine, body)) @@ -313,10 +305,9 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTime12Hour(t *testing.T) teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, false, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, false, translateFunc, "user-avatar.png") require.NoError(t, err) - require.Contains(t, body, "sender - 2:30 PM", fmt.Sprintf("Expected email text 'sender - 2:30 PM'. Got %s", body)) - require.Contains(t, body, "April 25", fmt.Sprintf("Expected email text 'April 25'. Got %s", body)) + require.Contains(t, body, "2:30 PM", fmt.Sprintf("Expected email text '2:30 PM'. Got %s", body)) } func TestGetNotificationEmailBodyFullNotificationLocaleTime24Hour(t *testing.T) { @@ -347,10 +338,9 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTime24Hour(t *testing.T) teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) - require.Contains(t, body, "sender - 14:30", fmt.Sprintf("Expected email text 'sender - 14:30'. Got %s", body)) - require.Contains(t, body, "April 25", fmt.Sprintf("Expected email text 'April 25'. Got %s", body)) + require.Contains(t, body, "14:30", fmt.Sprintf("Expected email text '14:30'. Got %s", body)) } // from here @@ -378,11 +368,9 @@ func TestGetNotificationEmailBodyGenericNotificationPublicChannel(t *testing.T) teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) - require.Contains(t, body, "You have a new notification from "+senderName, fmt.Sprintf("Expected email text 'You have a new notification from %s'. Got %s", senderName, body)) - require.False(t, strings.Contains(body, "Channel: "+channel.DisplayName), fmt.Sprintf("Did not expect email text 'CHANNEL: %s'. Got %s", channel.DisplayName, body)) - require.False(t, strings.Contains(body, post.Message), fmt.Sprintf("Did not expect email text '%s'. Got %s", post.Message, body)) + require.Contains(t, body, "mentioned you in a message", fmt.Sprintf("Expected email text 'mentioned you in a message. Got %s", body)) require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } @@ -410,11 +398,9 @@ func TestGetNotificationEmailBodyGenericNotificationGroupChannel(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) - require.Contains(t, body, "You have a new Group Message from "+senderName, fmt.Sprintf("Expected email text 'You have a new Group Message from %s'. Got %s", senderName, body)) - require.False(t, strings.Contains(body, "CHANNEL: "+channel.DisplayName), fmt.Sprintf("Did not expect email text 'CHANNEL: %s'. Got %s", channel.DisplayName, body)) - require.False(t, strings.Contains(body, post.Message), fmt.Sprintf("Did not expect email text '%s'. Got %s", post.Message, body)) + require.Contains(t, body, "sent you a new message", fmt.Sprintf("Expected email text 'sent you a new message. Got "+body)) require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } @@ -442,11 +428,9 @@ func TestGetNotificationEmailBodyGenericNotificationPrivateChannel(t *testing.T) teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) - require.Contains(t, body, "You have a new notification from "+senderName, fmt.Sprintf("Expected email text 'You have a new notification from %s'. Got %s", senderName, body)) - require.False(t, strings.Contains(body, "CHANNEL: "+channel.DisplayName), fmt.Sprintf("Did not expect email text 'CHANNEL: %s'. Got %s", channel.DisplayName, body)) - require.False(t, strings.Contains(body, post.Message), fmt.Sprintf("Did not expect email text '%s'. Got %s", post.Message, body)) + require.Contains(t, body, "mentioned you in a message", fmt.Sprintf("Expected email text 'mentioned you in a message. Got %s", body)) require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } @@ -474,11 +458,9 @@ func TestGetNotificationEmailBodyGenericNotificationDirectChannel(t *testing.T) teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) - require.Contains(t, body, "You have a new Direct Message from "+senderName, fmt.Sprintf("Expected email text 'You have a new Direct Message from "+senderName+"'. Got "+body)) - require.False(t, strings.Contains(body, "CHANNEL: "+channel.DisplayName), fmt.Sprintf("Did not expect email text 'CHANNEL: %s'. Got %s", channel.DisplayName, body)) - require.False(t, strings.Contains(body, post.Message), fmt.Sprintf("Did not expect email text '%s'. Got %s", post.Message, body)) + require.Contains(t, body, "sent you a new message", fmt.Sprintf("Expected email text 'sent you a new message. Got "+body)) require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } @@ -510,7 +492,7 @@ func TestGetNotificationEmailEscapingChars(t *testing.T) { body, err := th.App.getNotificationEmailBody(recipient, post, ch, channelName, senderName, teamName, teamURL, - emailNotificationContentsType, true, translateFunc) + emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) assert.NotContains(t, body, message) @@ -554,7 +536,7 @@ func TestGetNotificationEmailBodyPublicChannelMention(t *testing.T) { body, err := th.App.getNotificationEmailBody(recipient, post, ch, ch.Name, senderName, teamName, teamURL, - emailNotificationContentsType, true, translateFunc) + emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) channelURL := teamURL + "/channels/" + ch.Name mention := "~" + ch.Name @@ -620,7 +602,7 @@ func TestGetNotificationEmailBodyMultiPublicChannelMention(t *testing.T) { body, err := th.App.getNotificationEmailBody(recipient, post, ch, ch.Name, senderName, teamName, teamURL, - emailNotificationContentsType, true, translateFunc) + emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) channelURL := teamURL + "/channels/" + ch.Name channelURL2 := teamURL + "/channels/" + ch2.Name @@ -669,7 +651,7 @@ func TestGetNotificationEmailBodyPrivateChannelMention(t *testing.T) { body, err := th.App.getNotificationEmailBody(recipient, post, ch, ch.Name, senderName, teamName, teamURL, - emailNotificationContentsType, true, translateFunc) + emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) channelURL := teamURL + "/channels/" + ch.Name mention := "~" + ch.Name @@ -813,7 +795,7 @@ func TestLandingLink(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, teamURL, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } @@ -843,7 +825,7 @@ func TestLandingLinkPermalink(t *testing.T) { teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil) storeMock.On("Team").Return(&teamStoreMock) - body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc) + body, err := th.App.getNotificationEmailBody(recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png") require.NoError(t, err) require.Contains(t, body, teamURL+"/pl/"+post.Id, fmt.Sprintf("Expected email text '%s'. Got %s", teamURL, body)) } diff --git a/app/server.go b/app/server.go index 829cbd5153f..f76e6562c68 100644 --- a/app/server.go +++ b/app/server.go @@ -4,12 +4,14 @@ package app import ( + "bytes" "context" "crypto/tls" "encoding/json" "fmt" "hash/maphash" "html/template" + "io" "io/ioutil" "net" "net/http" @@ -2114,6 +2116,76 @@ func (a *App) generateSupportPacketYaml() (*model.FileData, string) { return nil, warning } +func (s *Server) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) { + if *s.Config().FileSettings.DriverName == "" { + img, appErr := s.GetDefaultProfileImage(user) + if appErr != nil { + return nil, false, appErr + } + return img, false, nil + } + + path := "users/" + user.Id + "/profile.png" + + data, err := s.ReadFile(path) + if err != nil { + img, appErr := s.GetDefaultProfileImage(user) + if appErr != nil { + return nil, false, appErr + } + + if user.LastPictureUpdate == 0 { + if _, err := s.WriteFile(bytes.NewReader(img), path); err != nil { + return nil, false, err + } + } + return img, true, nil + } + + return data, false, nil +} + +func (s *Server) GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError) { + var img []byte + var appErr *model.AppError + + if user.IsBot { + img = model.BotDefaultImage + appErr = nil + } else { + img, appErr = CreateProfileImage(user.Username, user.Id, *s.Config().FileSettings.InitialFont) + } + if appErr != nil { + return nil, appErr + } + return img, nil +} + +func (s *Server) ReadFile(path string) ([]byte, *model.AppError) { + backend, err := s.FileBackend() + if err != nil { + return nil, err + } + result, nErr := backend.ReadFile(path) + if nErr != nil { + return nil, model.NewAppError("ReadFile", "api.file.read_file.app_error", nil, nErr.Error(), http.StatusInternalServerError) + } + return result, nil +} + +func (s *Server) WriteFile(fr io.Reader, path string) (int64, *model.AppError) { + backend, err := s.FileBackend() + if err != nil { + return 0, err + } + + result, nErr := backend.WriteFile(fr, path) + if nErr != nil { + return result, model.NewAppError("WriteFile", "api.file.write_file.app_error", nil, nErr.Error(), http.StatusInternalServerError) + } + return result, nil +} + func runDNDStatusExpireJob(a *App) { if a.IsLeader() { a.srv.dndnTaskMut.Lock() diff --git a/app/user.go b/app/user.go index e2b44d63430..1840741bc52 100644 --- a/app/user.go +++ b/app/user.go @@ -875,48 +875,11 @@ func getFont(initialFont string) (*truetype.Font, error) { } func (a *App) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) { - if *a.Config().FileSettings.DriverName == "" { - img, appErr := a.GetDefaultProfileImage(user) - if appErr != nil { - return nil, false, appErr - } - return img, false, nil - } - - path := "users/" + user.Id + "/profile.png" - - data, err := a.ReadFile(path) - if err != nil { - img, appErr := a.GetDefaultProfileImage(user) - if appErr != nil { - return nil, false, appErr - } - - if user.LastPictureUpdate == 0 { - if _, err := a.WriteFile(bytes.NewReader(img), path); err != nil { - return nil, false, err - } - } - return img, true, nil - } - - return data, false, nil + return a.srv.GetProfileImage(user) } func (a *App) GetDefaultProfileImage(user *model.User) ([]byte, *model.AppError) { - var img []byte - var appErr *model.AppError - - if user.IsBot { - img = model.BotDefaultImage - appErr = nil - } else { - img, appErr = CreateProfileImage(user.Username, user.Id, *a.Config().FileSettings.InitialFont) - } - if appErr != nil { - return nil, appErr - } - return img, nil + return a.srv.GetDefaultProfileImage(user) } func (a *App) SetDefaultProfileImage(user *model.User) *model.AppError { diff --git a/i18n/en.json b/i18n/en.json index 8740b6d8a91..3e74d9428c1 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1611,31 +1611,16 @@ "translation": "Email batching has been disabled by the system administrator." }, { - "id": "api.email_batching.render_batched_post.date", - "translation": "{{.Hour}}:{{.Minute}} {{.Timezone}}, {{.Month}} {{.Day}}" + "id": "api.email_batching.send_batched_email_notification.button", + "translation": "Open Mattermost" }, { - "id": "api.email_batching.render_batched_post.direct_message", - "translation": "Direct Message from " + "id": "api.email_batching.send_batched_email_notification.messageButton", + "translation": "View this message" }, { - "id": "api.email_batching.render_batched_post.go_to_post", - "translation": "Go to Post" - }, - { - "id": "api.email_batching.render_batched_post.group_message", - "translation": "Group Message from " - }, - { - "id": "api.email_batching.render_batched_post.notification", - "translation": "Notification from " - }, - { - "id": "api.email_batching.send_batched_email_notification.body_text", - "translation": { - "one": "You have a new notification.", - "other": "You have {{.Count}} new notifications." - } + "id": "api.email_batching.send_batched_email_notification.subTitle", + "translation": "See below for a summary of your new messages." }, { "id": "api.email_batching.send_batched_email_notification.subject", @@ -1644,6 +1629,17 @@ "other": "[{{.SiteName}}] New Notifications for {{.Month}} {{.Day}}, {{.Year}}" } }, + { + "id": "api.email_batching.send_batched_email_notification.time", + "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}" + }, + { + "id": "api.email_batching.send_batched_email_notification.title", + "translation": { + "one": "{{ .SenderName }} sent you new message", + "other": "{{ .SenderName }} and {{.Count}} others sent you new messages" + } + }, { "id": "api.emoji.create.duplicate.app_error", "translation": "Unable to create emoji. Another emoji with the same name already exists." @@ -3488,7 +3484,7 @@ }, { "id": "api.templates.post_body.button", - "translation": "Go To Post" + "translation": "View Message" }, { "id": "api.templates.questions_footer.info", @@ -5447,60 +5443,44 @@ "translation": "No license present" }, { - "id": "app.notification.body.intro.direct.full", - "translation": "You have a new Direct Message." + "id": "app.notification.body.dm.subTitle", + "translation": "While you were away, {{.SenderName}} sent you a new Direct Message." }, { - "id": "app.notification.body.intro.direct.generic", - "translation": "You have a new Direct Message from {{.SenderName}}" + "id": "app.notification.body.dm.time", + "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}" }, { - "id": "app.notification.body.intro.group_message.full", - "translation": "You have a new Group Message." + "id": "app.notification.body.dm.title", + "translation": "{{.SenderName}} sent you a new message" }, { - "id": "app.notification.body.intro.group_message.generic", - "translation": "You have a new Group Message from {{.SenderName}}" + "id": "app.notification.body.group.subTitle", + "translation": "While you were away, {{.SenderName}} sent a message to your group." }, { - "id": "app.notification.body.intro.notification.full", - "translation": "You have a new notification." + "id": "app.notification.body.group.title", + "translation": "{{.SenderName}} sent you a new message" }, { - "id": "app.notification.body.intro.notification.generic", - "translation": "You have a new notification from {{.SenderName}}" + "id": "app.notification.body.mention.subTitle", + "translation": "While you were away, {{.SenderName}} mentioned you in the {{.ChannelName}} channel." }, { - "id": "app.notification.body.text.direct.full", - "translation": "{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + "id": "app.notification.body.mention.title", + "translation": "{{.SenderName}} mentioned you in a message" }, { - "id": "app.notification.body.text.direct.generic", - "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + "id": "app.notification.footer.info", + "translation": " and go to Account Settings > Notifications" }, { - "id": "app.notification.body.text.group_message.full", - "translation": "Channel: {{.ChannelName}}" + "id": "app.notification.footer.infoLogin", + "translation": "Login to Mattermost" }, { - "id": "app.notification.body.text.group_message.full2", - "translation": "{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" - }, - { - "id": "app.notification.body.text.group_message.generic", - "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" - }, - { - "id": "app.notification.body.text.notification.full", - "translation": "Channel: {{.ChannelName}}" - }, - { - "id": "app.notification.body.text.notification.full2", - "translation": "{{.SenderName}} - {{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" - }, - { - "id": "app.notification.body.text.notification.generic", - "translation": "{{.Hour}}:{{.Minute}} {{.TimeZone}}, {{.Month}} {{.Day}}" + "id": "app.notification.footer.title", + "translation": "Want to change your notifications settings?" }, { "id": "app.notification.subject.direct.full", diff --git a/templates/invite_body.html b/templates/invite_body.html index 5f4c8b03e79..af10cab286a 100644 --- a/templates/invite_body.html +++ b/templates/invite_body.html @@ -125,7 +125,7 @@ font-weight: 600 !important; font-size: 28px !important; line-height: 36px !important; - letter-spacing: -0.02em !important; + letter-spacing: -0.01em !important; color: #3D3C40 !important; } @@ -144,6 +144,17 @@ padding: 15px 24px !important; } + .messageButton a { + background-color: #FFFFFF !important; + border: 1px solid #FFFFFF !important; + box-sizing: border-box !important; + color: #166DE0 !important; + padding: 12px 20px !important; + font-weight: 600 !important; + font-size: 14px !important; + line-height: 14px !important; + } + .info div { font-size: 14px !important; line-height: 20px !important; @@ -212,13 +223,52 @@ width: 32px !important; } - .senderName div { + .postNameAndTime { + padding: 0px 0px 4px 0px !important; + display: flex; + } + + .senderName { + font-family: Open Sans, sans-serif; text-align: left !important; font-weight: 600 !important; font-size: 14px !important; line-height: 20px !important; color: #3D3C40 !important; - padding: 0px 0px 4px 0px !important; + } + + .time { + font-family: Open Sans, sans-serif; + font-size: 12px; + line-height: 16px; + color: rgba(61, 60, 64, 0.56); + padding: 2px 6px; + align-items: center; + float: left; + } + + .channelBg { + background: rgba(61, 60, 64, 0.08); + border-radius: 4px; + display: flex; + } + + .channelLogo { + width: 10px; + height: 10px; + padding: 5px 4px 5px 6px; + float: left; + } + + .channelName { + font-family: Open Sans, sans-serif; + font-weight: 600; + font-size: 10px; + line-height: 16px; + letter-spacing: 0.01em; + text-transform: uppercase; + color: rgba(61, 60, 64, 0.64); + padding: 2px 6px 2px 0px; } .senderMessage div { @@ -383,7 +433,7 @@ - + @@ -398,15 +448,41 @@ - + {{if .MessageURL}} + + + + {{end}}
-
{{.Props.SenderName}}
+
+
+
{{.SenderName}}
+ {{if .Time}} +
{{.Time}}
+ {{end}} + {{if .ChannelName}} +
+ +
{{.ChannelName}}
+
+ {{end}} +
-
{{.Props.Message}}
+
{{.Message}}
+ + + + +
+ + {{$.Props.MessageButton}} + +
+
diff --git a/templates/messages_notification.html b/templates/messages_notification.html new file mode 100644 index 00000000000..1e83de048b7 --- /dev/null +++ b/templates/messages_notification.html @@ -0,0 +1,550 @@ +{{define "messages_notification"}} + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + +
+
{{.Props.Title}}
+
+
{{.Props.SubTitle}}
+
+ + + + +
+ + {{.Props.Button}} + +
+
+
+ +
+
+ + {{range .Props.Posts}} +
+ +
+ + + + + + +
+ +
+ +
+ + + + + + +
+ + + + + + +
+ +
+
+
+ +
+ + + + + + + + + {{if .MessageURL}} + + + + {{end}} + +
+
+
{{.SenderName}}
+ {{if .Time}} +
{{.Time}}
+ {{end}} + {{if .ChannelName}} +
+ +
{{.ChannelName}}
+
+ {{end}} +
+
+
{{.Message}}
+
+ + + + +
+ + {{$.Props.MessageButton}} + +
+
+
+ +
+ +
+
+ +
{{end}} + +
+ + + + + + +
+ +
+ + + + + + + + + +
+
{{.Props.NotificationFooterTitle}}
+
+
{{.Props.NotificationFooterInfoLogin}}{{.Props.NotificationFooterInfo}}
+
+
+ +
+
+ +
+ + + + + + +
+ +
+ + + + + + +
+
{{.Props.Organization}} + {{.Props.FooterV2}} +
+
+
+ +
+
+ +
+
+ +
+ + + + +{{end}} diff --git a/templates/messages_notification.mjml b/templates/messages_notification.mjml new file mode 100644 index 00000000000..ad840ff4207 --- /dev/null +++ b/templates/messages_notification.mjml @@ -0,0 +1,25 @@ + + + + + + + + + {{range .Props.Posts}}
+ +
{{end}}
+ + + + {{.Props.NotificationFooterTitle}} + + + {{.Props.NotificationFooterInfoLogin}}{{.Props.NotificationFooterInfo}} + + + + +
+
+
diff --git a/templates/partials/card.mjml b/templates/partials/card.mjml index e3b0cc6aa6d..cf979cfcff2 100644 --- a/templates/partials/card.mjml +++ b/templates/partials/card.mjml @@ -1,15 +1,33 @@ - + - - {{.Props.SenderName}} - + + + +
+
{{.SenderName}}
+ {{if .Time}} +
{{.Time}}
+ {{end}} + {{if .ChannelName}} +
+ +
{{.ChannelName}}
+
+ {{end}} +
+ + +
- {{.Props.Message}} + {{.Message}} + {{if .MessageURL}} + {{$.Props.MessageButton}} + {{end}}
-
\ No newline at end of file + diff --git a/templates/partials/style.mjml b/templates/partials/style.mjml index 7a93d522fc2..26b58034df9 100644 --- a/templates/partials/style.mjml +++ b/templates/partials/style.mjml @@ -11,168 +11,217 @@ @import url(https://fonts.googleapis.com/css?family=Open+Sans:300,400,500,600,700); .emailBody { - background:#F3F3F3 !important; + background: #F3F3F3 !important; } .emailBody a{ - text-decoration:none !important; - color:#166DE0 !important; + text-decoration: none !important; + color: #166DE0 !important; } .title div { - font-weight:600 !important; - font-size:28px !important; - line-height:36px !important; - letter-spacing:-0.02em !important; - color:#3D3C40 !important; + font-weight: 600 !important; + font-size: 28px !important; + line-height: 36px !important; + letter-spacing: -0.01em !important; + color: #3D3C40 !important; } .subTitle div { - font-size:16px !important; - line-height:24px !important; - color:rgba(61, 60, 64, 0.64) !important; + font-size: 16px !important; + line-height: 24px !important; + color: rgba(61, 60, 64, 0.64) !important; } .button a { - background-color:#166DE0 !important; - font-weight:600 !important; - font-size:16px !important; - line-height:18px !important; - color:#FFFFFF !important; - padding:15px 24px !important; + background-color: #166DE0 !important; + font-weight: 600 !important; + font-size: 16px !important; + line-height: 18px !important; + color: #FFFFFF !important; + padding: 15px 24px !important; + } + + .messageButton a{ + background-color: #FFFFFF !important; + border: 1px solid #FFFFFF !important; + box-sizing: border-box !important; + color: #166DE0 !important; + padding: 12px 20px !important; + font-weight: 600 !important; + font-size: 14px !important; + line-height: 14px !important; } .info div { - font-size:14px !important; - line-height:20px !important; - color:#3D3C40 !important; - padding:40px 0px !important; + font-size: 14px !important; + line-height: 20px !important; + color: #3D3C40 !important; + padding: 40px 0px !important; } .footerTitle div { - font-weight:600 !important; - font-size:16px !important; - line-height:24px !important; - color:#3D3C40 !important; - padding:0px 0px 4px 0px !important; + font-weight: 600 !important; + font-size: 16px !important; + line-height: 24px !important; + color: #3D3C40 !important; + padding: 0px 0px 4px 0px !important; } .footerInfo div { - font-size:14px !important; - line-height:20px !important; - color:#3D3C40 !important; - padding:0px 48px 0px 48px !important; + font-size: 14px !important; + line-height: 20px !important; + color: #3D3C40 !important; + padding: 0px 48px 0px 48px !important; } .footerInfo a { - color:#166DE0 !important; + color: #166DE0 !important; } .appDownloadButton a{ - background-color:#FFFFFF !important; - border:1px solid #166DE0 !important; + background-color: #FFFFFF !important; + border: 1px solid #166DE0 !important; box-sizing: border-box !important; - color:#166DE0 !important; - padding:13px 20px !important; - font-weight:600 !important; - font-size:14px !important; - line-height:14px !important; + color: #166DE0 !important; + padding: 13px 20px !important; + font-weight: 600 !important; + font-size: 14px !important; + line-height: 14px !important; } .emailFooter div { - font-size:12px !important; - line-height:16px !important; - color:rgba(61, 60, 64, 0.56) !important; - padding:8px 24px 8px 24px !important; + font-size: 12px !important; + line-height: 16px !important; + color: rgba(61, 60, 64, 0.56) !important; + padding: 8px 24px 8px 24px !important; } .postCard { - padding:0px 24px 40px 24px !important; + padding: 0px 24px 40px 24px !important; } .messageCard { - background:#FFFFFF !important; - border:1px solid rgba(61, 60, 64, 0.08) !important; - box-sizing:border-box !important; - box-shadow:0px 8px 24px rgba(0, 0, 0, 0.12) !important; - border-radius:4px !important; - padding:32px !important; + background: #FFFFFF !important; + border: 1px solid rgba(61, 60, 64, 0.08) !important; + box-sizing: border-box !important; + box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.12) !important; + border-radius: 4px !important; + padding: 32px !important; } .messageAvatar img { - width:32px !important; - height:32px !important; - padding:0px !important; - border-radius:32px !important; + width: 32px !important; + height: 32px !important; + padding: 0px !important; + border-radius: 32px !important; } .messageAvatarCol { - width:32px !important; + width: 32px !important; } - .senderName div { - text-align:left !important; - font-weight:600 !important; - font-size:14px !important; - line-height:20px !important; - color:#3D3C40 !important; - padding:0px 0px 4px 0px !important; + .postNameAndTime { + padding: 0px 0px 4px 0px !important; + display: flex; + } + .senderName { + font-family: Open Sans, sans-serif; + text-align: left !important; + font-weight: 600 !important; + font-size: 14px !important; + line-height: 20px !important; + color: #3D3C40 !important; } + .time { + font-family: Open Sans, sans-serif; + font-size: 12px; + line-height: 16px; + color: rgba(61, 60, 64, 0.56); + padding: 2px 6px; + align-items: center; + float: left; + } + + .channelBg { + background: rgba(61, 60, 64, 0.08); + border-radius: 4px; + display: flex; + } + + .channelLogo { + width: 10px; + height: 10px; + padding: 5px 4px 5px 6px; + float: left; + } + + .channelName { + font-family: Open Sans, sans-serif; + font-weight: 600; + font-size: 10px; + line-height: 16px; + letter-spacing: 0.01em; + text-transform: uppercase; + color: rgba(61, 60, 64, 0.64); + padding: 2px 6px 2px 0px; + } + .senderMessage div { - text-align:left !important; - font-size:14px !important; - line-height:20px !important; - color:#3D3C40 !important; - padding:0px !important; + text-align: left !important; + font-size: 14px !important; + line-height: 20px !important; + color: #3D3C40 !important; + padding: 0px !important; } .senderInfoCol { - width:394px !important; - padding:0px 0px 0px 12px !important; + width: 394px !important; + padding: 0px 0px 0px 12px !important; } @media all and (min-width: 541px) { .emailBody { - padding:32px !important; + padding: 32px !important; } } @media all and (max-width: 540px) and (min-width: 401px) { .emailBody { - padding:16px !important; + padding: 16px !important; } .messageCard { - padding:16px !important; + padding: 16px !important; } .senderInfoCol { - width:80% !important; - padding:0px 0px 0px 12px !important; + width: 80% !important; + padding: 0px 0px 0px 12px !important; } } @media all and (max-width: 400px) { .emailBody { - padding:0px !important; + padding: 0px !important; } .footerInfo div { - padding:0px !important; + padding: 0px !important; } .messageCard { - padding:16px !important; + padding: 16px !important; } .postCard { - padding:0px 0px 40px 0px !important; + padding: 0px 0px 40px 0px !important; } .senderInfoCol { - width:80% !important; - padding:0px 0px 0px 12px !important; + width: 80% !important; + padding: 0px 0px 0px 12px !important; } } diff --git a/templates/post_batched_body.html b/templates/post_batched_body.html deleted file mode 100644 index 6efca451644..00000000000 --- a/templates/post_batched_body.html +++ /dev/null @@ -1,43 +0,0 @@ -{{define "post_batched_body"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

- {{.Props.BodyText}} -

- {{.Props.Posts}} -
-
-
-
- -{{end}} diff --git a/templates/post_batched_post_full.html b/templates/post_batched_post_full.html deleted file mode 100644 index 7e12da46eb7..00000000000 --- a/templates/post_batched_post_full.html +++ /dev/null @@ -1,38 +0,0 @@ -{{define "post_batched_post_full"}} - - - - - - - - - - -
- - {{.Props.ChannelName}} - -
-
- - @{{.Props.SenderName}} - - - {{.Props.Date}} - -
-
-
{{.Props.PostMessage}}
- - {{.Props.Button}} - -
- -{{end}} diff --git a/templates/post_batched_post_generic.html b/templates/post_batched_post_generic.html deleted file mode 100644 index 5d34af645d6..00000000000 --- a/templates/post_batched_post_generic.html +++ /dev/null @@ -1,37 +0,0 @@ -{{define "post_batched_post_generic"}} - - - - - - - - - - -
- - {{.Props.ChannelName}} - - - @{{.Props.SenderName}} - -
-
- - {{.Props.Date}} - -
-
- - {{.Props.Button}} - -
- -{{end}} diff --git a/templates/post_body_full.html b/templates/post_body_full.html deleted file mode 100644 index db51074f516..00000000000 --- a/templates/post_body_full.html +++ /dev/null @@ -1,45 +0,0 @@ -{{define "post_body_full"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.BodyText}}

-

{{.Props.Info1}}
{{.Props.Info2}}

{{.Props.PostMessage}}

-

- {{.Props.Button}} -

-
-
-
-
- -{{end}} - diff --git a/templates/post_body_generic.html b/templates/post_body_generic.html deleted file mode 100644 index dfea4e197b6..00000000000 --- a/templates/post_body_generic.html +++ /dev/null @@ -1,44 +0,0 @@ -{{define "post_body_generic"}} - - - - - -
- - - - -
- - - - - - - - - {{template "email_footer" . }} - -
- -
- - - - - - {{template "email_info" . }} - -
-

{{.Props.BodyText}}

-

{{.Props.Info}}

-

- {{.Props.Button}} -

-
-
-
-
- -{{end}} diff --git a/templates/reset_body.html b/templates/reset_body.html index f105d7a8965..6ec11290914 100644 --- a/templates/reset_body.html +++ b/templates/reset_body.html @@ -105,7 +105,7 @@ font-weight: 600 !important; font-size: 28px !important; line-height: 36px !important; - letter-spacing: -0.02em !important; + letter-spacing: -0.01em !important; color: #3D3C40 !important; } @@ -124,6 +124,17 @@ padding: 15px 24px !important; } + .messageButton a { + background-color: #FFFFFF !important; + border: 1px solid #FFFFFF !important; + box-sizing: border-box !important; + color: #166DE0 !important; + padding: 12px 20px !important; + font-weight: 600 !important; + font-size: 14px !important; + line-height: 14px !important; + } + .info div { font-size: 14px !important; line-height: 20px !important; @@ -192,13 +203,52 @@ width: 32px !important; } - .senderName div { + .postNameAndTime { + padding: 0px 0px 4px 0px !important; + display: flex; + } + + .senderName { + font-family: Open Sans, sans-serif; text-align: left !important; font-weight: 600 !important; font-size: 14px !important; line-height: 20px !important; color: #3D3C40 !important; - padding: 0px 0px 4px 0px !important; + } + + .time { + font-family: Open Sans, sans-serif; + font-size: 12px; + line-height: 16px; + color: rgba(61, 60, 64, 0.56); + padding: 2px 6px; + align-items: center; + float: left; + } + + .channelBg { + background: rgba(61, 60, 64, 0.08); + border-radius: 4px; + display: flex; + } + + .channelLogo { + width: 10px; + height: 10px; + padding: 5px 4px 5px 6px; + float: left; + } + + .channelName { + font-family: Open Sans, sans-serif; + font-weight: 600; + font-size: 10px; + line-height: 16px; + letter-spacing: 0.01em; + text-transform: uppercase; + color: rgba(61, 60, 64, 0.64); + padding: 2px 6px 2px 0px; } .senderMessage div { diff --git a/templates/verify_body.html b/templates/verify_body.html index 2a67d1565b6..52cb8d4587c 100644 --- a/templates/verify_body.html +++ b/templates/verify_body.html @@ -105,7 +105,7 @@ font-weight: 600 !important; font-size: 28px !important; line-height: 36px !important; - letter-spacing: -0.02em !important; + letter-spacing: -0.01em !important; color: #3D3C40 !important; } @@ -124,6 +124,17 @@ padding: 15px 24px !important; } + .messageButton a { + background-color: #FFFFFF !important; + border: 1px solid #FFFFFF !important; + box-sizing: border-box !important; + color: #166DE0 !important; + padding: 12px 20px !important; + font-weight: 600 !important; + font-size: 14px !important; + line-height: 14px !important; + } + .info div { font-size: 14px !important; line-height: 20px !important; @@ -192,13 +203,52 @@ width: 32px !important; } - .senderName div { + .postNameAndTime { + padding: 0px 0px 4px 0px !important; + display: flex; + } + + .senderName { + font-family: Open Sans, sans-serif; text-align: left !important; font-weight: 600 !important; font-size: 14px !important; line-height: 20px !important; color: #3D3C40 !important; - padding: 0px 0px 4px 0px !important; + } + + .time { + font-family: Open Sans, sans-serif; + font-size: 12px; + line-height: 16px; + color: rgba(61, 60, 64, 0.56); + padding: 2px 6px; + align-items: center; + float: left; + } + + .channelBg { + background: rgba(61, 60, 64, 0.08); + border-radius: 4px; + display: flex; + } + + .channelLogo { + width: 10px; + height: 10px; + padding: 5px 4px 5px 6px; + float: left; + } + + .channelName { + font-family: Open Sans, sans-serif; + font-weight: 600; + font-size: 10px; + line-height: 16px; + letter-spacing: 0.01em; + text-transform: uppercase; + color: rgba(61, 60, 64, 0.64); + padding: 2px 6px 2px 0px; } .senderMessage div { diff --git a/templates/welcome_body.html b/templates/welcome_body.html index 8a49c10b95a..d3b1e16eb31 100644 --- a/templates/welcome_body.html +++ b/templates/welcome_body.html @@ -105,7 +105,7 @@ font-weight: 600 !important; font-size: 28px !important; line-height: 36px !important; - letter-spacing: -0.02em !important; + letter-spacing: -0.01em !important; color: #3D3C40 !important; } @@ -124,6 +124,17 @@ padding: 15px 24px !important; } + .messageButton a { + background-color: #FFFFFF !important; + border: 1px solid #FFFFFF !important; + box-sizing: border-box !important; + color: #166DE0 !important; + padding: 12px 20px !important; + font-weight: 600 !important; + font-size: 14px !important; + line-height: 14px !important; + } + .info div { font-size: 14px !important; line-height: 20px !important; @@ -192,13 +203,52 @@ width: 32px !important; } - .senderName div { + .postNameAndTime { + padding: 0px 0px 4px 0px !important; + display: flex; + } + + .senderName { + font-family: Open Sans, sans-serif; text-align: left !important; font-weight: 600 !important; font-size: 14px !important; line-height: 20px !important; color: #3D3C40 !important; - padding: 0px 0px 4px 0px !important; + } + + .time { + font-family: Open Sans, sans-serif; + font-size: 12px; + line-height: 16px; + color: rgba(61, 60, 64, 0.56); + padding: 2px 6px; + align-items: center; + float: left; + } + + .channelBg { + background: rgba(61, 60, 64, 0.08); + border-radius: 4px; + display: flex; + } + + .channelLogo { + width: 10px; + height: 10px; + padding: 5px 4px 5px 6px; + float: left; + } + + .channelName { + font-family: Open Sans, sans-serif; + font-weight: 600; + font-size: 10px; + line-height: 16px; + letter-spacing: 0.01em; + text-transform: uppercase; + color: rgba(61, 60, 64, 0.64); + padding: 2px 6px 2px 0px; } .senderMessage div {