MM-36234,MM-37030,MM-37031: CRT, desktop thread notifications (#18088)

* CRT: desktop thread notifications

* Fixes go lint

* Adds default for desktop CRT notifications

* Adds email and push notifications for CRT threads

Adds user ids of thread followers with CRT to crtMentions so they will get
notified appropriately.

* Minor change

* Refactor a bit

CRTMentions.addMention had a bug on the return and de-duplication.
This commit fixes duplicate notifications by looking up if the user is to be
notified on CRT on both email and push notifications.

* Minor refactor

* Changes according to review comments

- Fixes adding to followers a user that had explicitly unfollowed a
  thread.
- Simplified send email according to email_threads option
- Send mentions and followers in separate arrays via the websocket
- Fixes push notifications message for push_threads

* Adds a comment on a buggy use case

* Updates comment to correct ticket link

* Fixes when user notifications is set to all

There was a bug where if user had set notifications to all
then they would receive desktop notifications even for non following threads.

A similar bug existed in push notifications, where if a user has set it
to all the threads setting would still be considered.

This commit fixes that by adding users to notificationsForCRT
StringArray when they have the non thread setting to 'all'.

* Fixes notifications to users unfollowing threads

Users which had previously explicitly unfollowed a thread
should not receive notifications about those threads.

* Update store mocks

* Fixes push notifications for CRT

Push notification about replies for CRT users should have a title of
"Reply to Thread".

CRT users with global user setting to 'UserNotifyAll' should not get
notifications for unfollowed threads.

This commit fixes those issues.

* Fixes i18n error

Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
Kyriakos Z
2021-08-19 17:28:46 +03:00
коммит произвёл GitHub
родитель fbbd1193dc
Коммит a0c5d8feab
11 изменённых файлов: 202 добавлений и 36 удалений

Просмотреть файл

@@ -27,6 +27,8 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
return []string{}, nil
}
isCRTAllowed := a.Config().FeatureFlags.CollapsedThreads && *a.Config().ServiceSettings.CollapsedThreads != model.CollapsedThreadsDisabled
pchan := make(chan store.StoreResult, 1)
go func() {
props, err := a.Srv().Store.User().GetAllProfilesInChannel(context.Background(), channel.Id, true)
@@ -61,6 +63,16 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
}()
}
var tchan chan store.StoreResult
if isCRTAllowed && post.RootId != "" {
tchan = make(chan store.StoreResult, 1)
go func() {
followers, err := a.Srv().Store.Thread().GetThreadFollowers(post.RootId, true)
tchan <- store.StoreResult{Data: followers, NErr: err}
close(tchan)
}()
}
result := <-pchan
if result.NErr != nil {
return nil, result.NErr
@@ -73,6 +85,15 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
}
channelMemberNotifyPropsMap := result.Data.(map[string]model.StringMap)
followers := make(model.StringArray, 0)
if tchan != nil {
result = <-tchan
if result.NErr != nil {
return nil, result.NErr
}
followers = result.Data.([]string)
}
groups := make(map[string]*model.Group)
if gchan != nil {
result = <-gchan
@@ -158,11 +179,13 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
}()
// find which users in the channel are set up to always receive mobile notifications
// excludes CRT users since those should be added in notificationsForCRT
for _, profile := range profileMap {
if (profile.NotifyProps[model.PushNotifyProp] == model.UserNotifyAll ||
channelMemberNotifyPropsMap[profile.Id][model.PushNotifyProp] == model.ChannelNotifyAll) &&
(post.UserId != profile.Id || post.GetProp("from_webhook") == "true") &&
!post.IsSystemMessage() {
!post.IsSystemMessage() &&
!(a.isCRTEnabledForUser(profile.Id) && post.RootId != "") {
allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
}
}
@@ -174,6 +197,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
threadParticipants := map[string]bool{post.UserId: true}
participantMemberships := map[string]*model.ThreadMembership{}
membershipsMutex := &sync.Mutex{}
followersMutex := &sync.Mutex{}
if *a.Config().ServiceSettings.ThreadAutoFollow && post.RootId != "" {
var rootMentions *ExplicitMentions
if parentPostList != nil {
@@ -230,6 +254,14 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
mac <- model.NewAppError("SendNotifications", "app.channel.autofollow.app_error", nil, err.Error(), http.StatusInternalServerError)
return
}
// add new followers to existing followers
if threadMembership.Following && !followers.Contains(userID) {
followersMutex.Lock()
followers = append(followers, userID)
followersMutex.Unlock()
}
membershipsMutex.Lock()
participantMemberships[userID] = threadMembership
membershipsMutex.Unlock()
@@ -255,6 +287,23 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
updateMentionChans = append(updateMentionChans, umc)
}
notificationsForCRT := &CRTNotifiers{}
if isCRTAllowed && post.RootId != "" {
for _, uid := range followers {
profile := profileMap[uid]
if profile == nil || !a.isCRTEnabledForUser(uid) {
continue
}
if post.GetProp("from_webhook") != "true" && uid == post.UserId {
continue
}
// add user id to notificationsForCRT depending on threads notify props
notificationsForCRT.addUserToNotify(profile, mentions)
}
}
notification := &PostNotification{
Post: post.Clone(),
Channel: channel,
@@ -263,7 +312,10 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
}
if *a.Config().EmailSettings.SendEmailNotifications {
for _, id := range mentionedUsersList {
emailReceipients := append(mentionedUsersList, notificationsForCRT.Email...)
emailReceipients = model.RemoveDuplicateStrings(emailReceipients)
for _, id := range emailReceipients {
if profileMap[id] == nil {
continue
}
@@ -362,7 +414,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
if sendPushNotifications {
for _, id := range mentionedUsersList {
if profileMap[id] == nil {
if profileMap[id] == nil || notificationsForCRT.Push.Contains(id) {
continue
}
@@ -402,7 +454,7 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
}
for _, id := range allActivityPushUserIds {
if profileMap[id] == nil {
if profileMap[id] == nil || notificationsForCRT.Push.Contains(id) {
continue
}
@@ -433,6 +485,37 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
}
}
}
for _, id := range notificationsForCRT.Push {
if profileMap[id] == nil {
continue
}
var status *model.Status
var err *model.AppError
if status, err = a.GetStatus(id); err != nil {
status = &model.Status{UserId: id, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
if DoesStatusAllowPushNotification(profileMap[id].NotifyProps, status, post.ChannelId) {
a.sendPushNotification(
notification,
profileMap[id],
profileMap[id].NotifyProps[model.PushThreadsNotifyProp] == model.UserNotifyMention,
false,
model.UserNotifyAll,
)
} else {
// register that a notification was not sent
a.NotificationsLog().Debug("Notification not sent",
mlog.String("ackId", ""),
mlog.String("type", model.PushTypeMessage),
mlog.String("userId", id),
mlog.String("postId", post.Id),
mlog.String("status", model.PushNotSent),
)
}
}
}
message := model.NewWebSocketEvent(model.WebsocketEventPosted, "", post.ChannelId, "", nil)
@@ -469,6 +552,10 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
message.Add("mentions", model.ArrayToJson(mentionedUsersList))
}
if len(notificationsForCRT.Desktop) != 0 {
message.Add("followers", model.ArrayToJson(notificationsForCRT.Desktop))
}
published, err := a.publishWebsocketEventForPermalinkPost(post, message)
if err != nil {
return nil, err
@@ -478,28 +565,25 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
}
// If this is a reply in a thread, notify participants
if a.Config().FeatureFlags.CollapsedThreads && *a.Config().ServiceSettings.CollapsedThreads != model.CollapsedThreadsDisabled && post.RootId != "" {
followers, err := a.Srv().Store.Thread().GetThreadFollowers(post.RootId)
if err != nil {
return nil, errors.Wrapf(err, "cannot get thread %q followers", post.RootId)
}
if isCRTAllowed && post.RootId != "" {
for _, uid := range followers {
sendEvent := *a.Config().ServiceSettings.CollapsedThreads == model.CollapsedThreadsDefaultOn
// check if a participant has overridden collapsed threads settings
if preference, prefErr := a.Srv().Store.Preference().Get(uid, model.PreferenceCategoryDisplaySettings, model.PreferenceNameCollapsedThreadsEnabled); prefErr == nil {
sendEvent = preference.Value == "on"
// A user following a thread but had left the channel won't get a notification
// https://mattermost.atlassian.net/browse/MM-36769
if profileMap[uid] == nil {
continue
}
if sendEvent {
if a.isCRTEnabledForUser(uid) {
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, team.Id, "", uid, nil)
threadMembership := participantMemberships[uid]
if threadMembership == nil {
threadMembership, err = a.Srv().Store.Thread().GetMembershipForUser(uid, post.RootId)
tm, err := a.Srv().Store.Thread().GetMembershipForUser(uid, post.RootId)
if err != nil {
return nil, errors.Wrapf(err, "Missing thread membership for participant in notifications. user_id=%q thread_id=%q", uid, post.RootId)
}
if threadMembership == nil {
if tm == nil {
continue
}
threadMembership = tm
}
userThread, err := a.Srv().Store.Thread().GetThreadForUser(channel.TeamId, threadMembership, true)
if err != nil {
@@ -525,14 +609,15 @@ func (a *App) SendNotifications(post *model.Post, team *model.Team, channel *mod
}
}
}
}
return mentionedUsersList, nil
}
func (a *App) userAllowsEmail(user *model.User, channelMemberNotificationProps model.StringMap, post *model.Post) bool {
userAllowsEmails := user.NotifyProps[model.EmailNotifyProp] != "false"
if channelEmail, ok := channelMemberNotificationProps[model.EmailNotifyProp]; ok {
// if CRT is ON for user and the post is a reply disregard the channelEmail setting
if channelEmail, ok := channelMemberNotificationProps[model.EmailNotifyProp]; ok && !(a.isCRTEnabledForUser(user.Id) && post.RootId != "") {
if channelEmail != model.ChannelNotifyDefault {
userAllowsEmails = channelEmail != "false"
}
@@ -1228,3 +1313,40 @@ func (a *App) GetNotificationNameFormat(user *model.User) string {
return data.Value
}
type CRTNotifiers struct {
// Desktop contains the user IDs of thread followers to receive desktop notification
Desktop model.StringArray
// Email contains the user IDs of thread followers to receive email notification
Email model.StringArray
// Push contains the user IDs of thread followers to receive push notification
Push model.StringArray
}
func (c *CRTNotifiers) addUserToNotify(user *model.User, mentions *ExplicitMentions) {
// user notify props
desktop := user.NotifyProps[model.DesktopNotifyProp]
push := user.NotifyProps[model.PushNotifyProp]
shouldEmail := user.NotifyProps[model.EmailNotifyProp] == "true"
// user thread notify props
desktopThreads := user.NotifyProps[model.DesktopThreadsNotifyProp]
emailThreads := user.NotifyProps[model.EmailThreadsNotifyProp]
pushThreads := user.NotifyProps[model.PushThreadsNotifyProp]
_, userWasMentioned := mentions.Mentions[user.Id]
if desktop != model.UserNotifyNone && (userWasMentioned || desktopThreads == model.UserNotifyAll || desktop == model.UserNotifyAll) {
c.Desktop = append(c.Desktop, user.Id)
}
if shouldEmail && (userWasMentioned || emailThreads == model.UserNotifyAll) {
c.Email = append(c.Email, user.Id)
}
if push != model.UserNotifyNone && (userWasMentioned || pushThreads == model.UserNotifyAll || push == model.UserNotifyAll) {
c.Push = append(c.Push, user.Id)
}
}

Просмотреть файл

@@ -236,7 +236,10 @@ func (a *App) getNotificationEmailBody(recipient *model.User, post *model.Post,
data.Props["NotificationFooterInfoLogin"] = translateFunc("app.notification.footer.infoLogin")
data.Props["NotificationFooterInfo"] = translateFunc("app.notification.footer.info")
if channel.Type == model.ChannelTypeDirect {
if a.isCRTEnabledForUser(recipient.Id) && post.RootId != "" {
data.Props["Title"] = translateFunc("app.notification.body.thread.title", map[string]interface{}{"SenderName": senderName})
data.Props["SubTitle"] = translateFunc("app.notification.body.thread.subTitle", map[string]interface{}{"SenderName": senderName})
} else if channel.Type == model.ChannelTypeDirect {
// 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})

Просмотреть файл

@@ -198,6 +198,10 @@ func (a *App) getPushNotificationMessage(contentsConfig, postMessage string, exp
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_thread")
}
if replyToThreadType == model.UserNotifyAll {
return senderName + userLocale("api.post.send_notification_and_forget.push_comment_on_crt_thread")
}
return senderName + userLocale("api.post.send_notifications_and_forget.push_general_message")
}
@@ -559,9 +563,13 @@ func (a *App) buildFullPushNotificationMessage(contentsConfig string, post *mode
IsIdLoaded: false,
}
userLocale := i18n.GetUserTranslations(user.Locale)
cfg := a.Config()
if contentsConfig != model.GenericNoChannelNotification || channel.Type == model.ChannelTypeDirect {
msg.ChannelName = channelName
if a.isCRTEnabledForUser(user.Id) && post.RootId != "" {
msg.ChannelName = userLocale("api.push_notification.title.collapsed_threads")
}
}
msg.SenderName = senderName
@@ -591,7 +599,6 @@ func (a *App) buildFullPushNotificationMessage(contentsConfig string, post *mode
}
}
userLocale := i18n.GetUserTranslations(user.Locale)
hasFiles := post.FileIds != nil && len(post.FileIds) > 0
msg.Message = a.getPushNotificationMessage(