[MM-47751][MM-48102] MPA: Send Persistent Notifications (#21619)

* MM-46410: adds urgency on mention counts

We have introduced priority for posts in
https://github.com/mattermost/mattermost-webapp/pull/10951.
We do need to color the mention badges in the webapp with a prominent
color when a mention is posted in an urgent message.
A thread has urgent mentions if the root post is marked as urgent, and
the replies contain mentions to the user viewing the thread.

This PR adds two columns, urgentmentioncount, and isurgent, in
channelmembers, and threads tables respectively.
Furthermore when asking for team/thread mention counts, we also return
urgent mention counts for the user.

* Adds PostAcknowledgements table and apis

* job init and fetch mentions

* add-migrations

* delete-expired

* send-notifications

* Fetches post priority in batches

* stop-notifications

* stop-notification-on-reply

* MM-47750: Adds PostAcknowledgements table and apis

- Adds post acknowledgement api/app/store methods to be able to save and
delete post acknowledgements by users.
- Adds wesbsocket events for acknowledgement created/deleted
- Returns post acknowledgements in the post's metadata

* add-license-check

* add-pagination

* delete on channel and team

* validate guests

* add configs

* move create priority post check from app to api

* Add desktop notifications

* check status

* use config in job

* add IsUrgent check

* Add last-sent-at

* validate max recipients

* Update lastSentAt

* Validate min. recipient

* send email notification only once

* remove email notifications

* use latest time from config to run job

* Add notifications counter

* publish events to mentioned users only

* pickup license updates in scheduler

* don't allow post owner to stop notifications

* follow normal notifications behaviour

* Validates persistent notifications interval

* move logic of handling valid and expired posts into sql

* Adds persistent notifications in the webapp

---------

Co-authored-by: koox00 <3829551+koox00@users.noreply.github.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
Этот коммит содержится в:
Vishal
2023-05-18 23:44:12 +05:30
коммит произвёл GitHub
родитель ce165302cf
Коммит 9399ce8637
74 изменённых файлов: 4052 добавлений и 888 удалений

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

@@ -122,6 +122,8 @@ type AppIface interface {
// DeleteGroupConstrainedMemberships deletes team and channel memberships of users who aren't members of the allowed
// groups of all group-constrained teams and channels.
DeleteGroupConstrainedMemberships(c *request.Context) error
// DeletePersistentNotification stops the persistent notifications.
DeletePersistentNotification(c request.CTX, post *model.Post) *model.AppError
// DeletePublicKey will delete plugin public key from the config.
DeletePublicKey(name string) *model.AppError
// DemoteUserToGuest Convert user's roles and all his membership's roles from
@@ -297,6 +299,9 @@ type AppIface interface {
RenameChannel(c request.CTX, channel *model.Channel, newChannelName string, newDisplayName string) (*model.Channel, *model.AppError)
// RenameTeam is used to rename the team Name and the DisplayName fields
RenameTeam(team *model.Team, newTeamName string, newDisplayName string) (*model.Team, *model.AppError)
// ResolvePersistentNotification stops the persistent notifications, if a loggedInUserID(except the post owner) reacts, reply or ack on the post.
// Post-owner can only delete the original post to stop the notifications.
ResolvePersistentNotification(c request.CTX, post *model.Post, loggedInUserID string) *model.AppError
// RevokeSessionsFromAllUsers will go through all the sessions active
// in the server and revoke them
RevokeSessionsFromAllUsers() *model.AppError
@@ -898,8 +903,10 @@ type AppIface interface {
IsFirstUserAccount() bool
IsLeader() bool
IsPasswordValid(password string) *model.AppError
IsPersistentNotificationsEnabled() bool
IsPhase2MigrationCompleted() *model.AppError
IsPluginActive(pluginName string) (bool, error)
IsPostPriorityEnabled() bool
IsUserSignUpAllowed() *model.AppError
JoinChannel(c request.CTX, channel *model.Channel, userID string) *model.AppError
JoinDefaultChannels(c request.CTX, teamID string, user *model.User, shouldBeAdmin bool, userRequestorId string) *model.AppError
@@ -1053,6 +1060,7 @@ type AppIface interface {
SendNotifyAdminPosts(c *request.Context, workspaceName string, currentSKU string, trial bool) *model.AppError
SendPasswordReset(email string, siteURL string) (bool, *model.AppError)
SendPaymentFailedEmail(failedPayment *model.FailedPayment) *model.AppError
SendPersistentNotifications() error
SendTestPushNotification(deviceID string) string
SendUpgradeConfirmationEmail(isYearly bool) *model.AppError
ServeInterPluginRequest(w http.ResponseWriter, r *http.Request, sourcePluginId, destinationPluginId string)

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

@@ -1521,11 +1521,16 @@ func (a *App) DeleteChannel(c request.CTX, channel *model.Channel, userID string
}
}
if err := a.Srv().Store().PostPersistentNotification().DeleteByChannel([]string{channel.Id}); err != nil {
return model.NewAppError("DeleteChannel", "app.post_persistent_notification.delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
deleteAt := model.GetMillis()
if err := a.Srv().Store().Channel().Delete(channel.Id, deleteAt); err != nil {
return model.NewAppError("DeleteChannel", "app.channel.delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
a.Srv().Platform().InvalidateCacheForChannel(channel)
message := model.NewWebSocketEvent(model.WebsocketEventChannelDeleted, channel.TeamId, "", "", nil, "")
@@ -2791,7 +2796,7 @@ func (a *App) markChannelAsUnreadFromPostCRTUnsupported(c request.CTX, postID st
if mErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
}
thread, mErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.isPostPriorityEnabled())
thread, mErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.IsPostPriorityEnabled())
if mErr != nil {
return nil, model.NewAppError("MarkChannelAsUnreadFromPost", "app.channel.update_last_viewed_at_post.app_error", nil, "", http.StatusInternalServerError).Wrap(mErr)
}
@@ -3095,6 +3100,10 @@ func (a *App) PermanentDeleteChannel(c request.CTX, channel *model.Channel) *mod
return model.NewAppError("PermanentDeleteChannel", "app.webhooks.permanent_delete_outgoing_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
if err := a.Srv().Store().PostPersistentNotification().DeleteByChannel([]string{channel.Id}); err != nil {
return model.NewAppError("PermanentDeleteChannel", "app.post_persistent_notification.delete_by_channel.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
deleteAt := model.GetMillis()
if nErr := a.Srv().Store().Channel().PermanentDelete(channel.Id); nErr != nil {

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

@@ -607,7 +607,7 @@ func (a *App) SendNotifications(c request.CTX, post *model.Post, team *model.Tea
}
threadMembership = tm
}
userThread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.isPostPriorityEnabled())
userThread, err := a.Srv().Store().Thread().GetThreadForUser(threadMembership, true, a.IsPostPriorityEnabled())
if err != nil {
return nil, errors.Wrapf(err, "cannot get thread %q for user %q", post.RootId, uid)
}

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

@@ -3285,6 +3285,28 @@ func (a *OpenTracingAppLayer) DeleteOutgoingWebhook(hookID string) *model.AppErr
return resultVar0
}
func (a *OpenTracingAppLayer) DeletePersistentNotification(c request.CTX, post *model.Post) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeletePersistentNotification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.DeletePersistentNotification(c, post)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) DeletePluginKey(pluginID string, key string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DeletePluginKey")
@@ -12121,6 +12143,23 @@ func (a *OpenTracingAppLayer) IsPasswordValid(password string) *model.AppError {
return resultVar0
}
func (a *OpenTracingAppLayer) IsPersistentNotificationsEnabled() bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsPersistentNotificationsEnabled")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsPersistentNotificationsEnabled()
return resultVar0
}
func (a *OpenTracingAppLayer) IsPhase2MigrationCompleted() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsPhase2MigrationCompleted")
@@ -12165,6 +12204,23 @@ func (a *OpenTracingAppLayer) IsPluginActive(pluginName string) (bool, error) {
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) IsPostPriorityEnabled() bool {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsPostPriorityEnabled")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.IsPostPriorityEnabled()
return resultVar0
}
func (a *OpenTracingAppLayer) IsUserSignUpAllowed() *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.IsUserSignUpAllowed")
@@ -14397,6 +14453,28 @@ func (a *OpenTracingAppLayer) ResetSamlAuthDataToEmail(includeDeleted bool, dryR
return resultVar0, resultVar1
}
func (a *OpenTracingAppLayer) ResolvePersistentNotification(c request.CTX, post *model.Post, loggedInUserID string) *model.AppError {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ResolvePersistentNotification")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.ResolvePersistentNotification(c, post, loggedInUserID)
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) RestoreChannel(c request.CTX, channel *model.Channel, userID string) (*model.Channel, *model.AppError) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.RestoreChannel")
@@ -15761,6 +15839,28 @@ func (a *OpenTracingAppLayer) SendPaymentFailedEmail(failedPayment *model.Failed
return resultVar0
}
func (a *OpenTracingAppLayer) SendPersistentNotifications() error {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendPersistentNotifications")
a.ctx = newCtx
a.app.Srv().Store().SetContext(newCtx)
defer func() {
a.app.Srv().Store().SetContext(origCtx)
a.ctx = origCtx
}()
defer span.Finish()
resultVar0 := a.app.SendPersistentNotifications()
if resultVar0 != nil {
span.LogFields(spanlog.Error(resultVar0))
ext.Error.Set(span, true)
}
return resultVar0
}
func (a *OpenTracingAppLayer) SendSubscriptionHistoryEvent(userID string) (*model.SubscriptionHistory, error) {
origCtx := a.ctx
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.SendSubscriptionHistoryEvent")

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

@@ -196,6 +196,21 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel
a.Srv().seenPendingPostIdsCache.SetWithExpiry(post.PendingPostId, savedPost.Id, PendingPostIDsCacheTTL)
}()
// Validate recipients counts in case it's not DM
if persistentNotification := post.GetPersistentNotification(); persistentNotification != nil && *persistentNotification && channel.Type != model.ChannelTypeDirect {
err := a.forEachPersistentNotificationPost([]*model.Post{post}, func(_ *model.Post, _ *model.Channel, _ *model.Team, mentions *ExplicitMentions, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
if maxRecipients := *a.Config().ServiceSettings.PersistentNotificationMaxRecipients; len(mentions.Mentions) > maxRecipients {
return model.NewAppError("CreatePost", "api.post.post_priority.max_recipients_persistent_notification_post.request_error", map[string]any{"MaxRecipients": maxRecipients}, "", http.StatusBadRequest)
} else if len(mentions.Mentions) == 0 {
return model.NewAppError("CreatePost", "api.post.post_priority.min_recipients_persistent_notification_post.request_error", nil, "", http.StatusBadRequest)
}
return nil
})
if err != nil {
return nil, model.NewAppError("CreatePost", "api.post.post_priority.persistent_notification_validation_error.request_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
post.SanitizeProps()
var pchan chan store.StoreResult
@@ -279,10 +294,6 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel
}
}
if !a.isPostPriorityEnabled() && post.GetPriority() != nil {
post.Metadata.Priority = nil
}
var metadata *model.PostMetadata
if post.Metadata != nil {
metadata = post.Metadata.Copy()
@@ -374,6 +385,12 @@ func (a *App) CreatePost(c request.CTX, post *model.Post, channel *model.Channel
// so we just return the one that was passed with post
rpost = a.PreparePostForClient(c, rpost, true, false, false)
if rpost.RootId != "" {
if appErr := a.ResolvePersistentNotification(c, parentPostList.Posts[post.RootId], rpost.UserId); appErr != nil {
return nil, appErr
}
}
// Make sure poster is following the thread
if *a.Config().ServiceSettings.ThreadAutoFollow && rpost.RootId != "" {
_, err := a.Srv().Store().Thread().MaintainMembership(user.Id, rpost.RootId, store.ThreadMembershipOpts{
@@ -1288,6 +1305,12 @@ func (a *App) DeletePost(c request.CTX, postID, deleteByID string) (*model.Post,
}
}
if post.RootId == "" {
if appErr := a.DeletePersistentNotification(c, post); appErr != nil {
return nil, appErr
}
}
postJSON, err := json.Marshal(post)
if err != nil {
return nil, model.NewAppError("DeletePost", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
@@ -1792,7 +1815,7 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model
}
var urgentCount int
if a.isPostPriorityEnabled() {
if a.IsPostPriorityEnabled() {
urgentCount, nErr = a.Srv().Store().Channel().CountUrgentPostsAfter(post.ChannelId, post.CreateAt-1, channel.GetOtherUserIdForDM(user.Id))
if nErr != nil {
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.count_urgent_posts_since.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
@@ -1832,7 +1855,7 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model
count += 1
if post.RootId == "" {
countRoot += 1
if a.isPostPriorityEnabled() {
if a.IsPostPriorityEnabled() {
priority, err := a.GetPriorityForPost(post.Id)
if err != nil {
return 0, 0, 0, err
@@ -1868,7 +1891,7 @@ func (a *App) countMentionsFromPost(c request.CTX, user *model.User, post *model
}
}
if a.isPostPriorityEnabled() {
if a.IsPostPriorityEnabled() {
priorityList, nErr := a.Srv().Store().PostPriority().GetForPosts(mentionPostIds)
if nErr != nil {
return 0, 0, 0, model.NewAppError("countMentionsFromPost", "app.channel.get_priority_for_posts.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
@@ -2277,7 +2300,3 @@ func includeEmbedsAndImages(a *App, c request.CTX, topThreadList *model.TopThrea
}
return topThreadList, nil
}
func (a *App) isPostPriorityEnabled() bool {
return a.Config().FeatureFlags.PostPriority && *a.Config().ServiceSettings.PostPriority
}

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

@@ -31,7 +31,6 @@ func (a *App) SaveAcknowledgementForPost(c *request.Context, postID, userID stri
acknowledgedAt := model.GetMillis()
acknowledgement, nErr := a.Srv().Store().PostAcknowledgement().Save(postID, userID, acknowledgedAt)
if nErr != nil {
var appErr *model.AppError
switch {
@@ -42,6 +41,10 @@ func (a *App) SaveAcknowledgementForPost(c *request.Context, postID, userID stri
}
}
if appErr := a.ResolvePersistentNotification(c, post, userID); appErr != nil {
return nil, appErr
}
// The post is always modified since the UpdateAt always changes
a.invalidateCacheForChannelPosts(channel.Id)

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

@@ -63,7 +63,7 @@ func (a *App) PreparePostListForClient(c request.CTX, originalList *model.PostLi
list.Posts[id] = post
}
if a.isPostPriorityEnabled() {
if a.IsPostPriorityEnabled() {
priority, _ := a.GetPriorityForPostList(list)
acknowledgements, _ := a.GetAcknowledgementsForPostList(list)
@@ -139,7 +139,7 @@ func (a *App) PreparePostForClient(c request.CTX, originalPost *model.Post, isNe
post.Metadata.Files = fileInfos
}
if includePriority && a.isPostPriorityEnabled() && post.RootId == "" {
if includePriority && a.IsPostPriorityEnabled() && post.RootId == "" {
// Post's Priority if any
if priority, err := a.GetPriorityForPost(post.Id); err != nil {
mlog.Warn("Failed to get post priority for a post", mlog.String("post_id", post.Id), mlog.Err(err))

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

@@ -0,0 +1,396 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"context"
"net/http"
"time"
"github.com/mattermost/mattermost-server/server/public/model"
"github.com/mattermost/mattermost-server/server/public/shared/mlog"
"github.com/mattermost/mattermost-server/server/v8/channels/app/request"
"github.com/mattermost/mattermost-server/server/v8/channels/store"
"github.com/pkg/errors"
)
// ResolvePersistentNotification stops the persistent notifications, if a loggedInUserID(except the post owner) reacts, reply or ack on the post.
// Post-owner can only delete the original post to stop the notifications.
func (a *App) ResolvePersistentNotification(c request.CTX, post *model.Post, loggedInUserID string) *model.AppError {
// Ignore the post owner's actions to their own post
if loggedInUserID == post.UserId {
return nil
}
if !a.IsPersistentNotificationsEnabled() {
return nil
}
_, err := a.Srv().Store().PostPersistentNotification().GetSingle(post.Id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
// Either the notification post is already deleted or was never a notification post
return nil
default:
return model.NewAppError("ResolvePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if !*a.Config().ServiceSettings.AllowPersistentNotificationsForGuests {
user, nErr := a.Srv().Store().User().Get(context.Background(), loggedInUserID)
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(nErr, &nfErr):
return model.NewAppError("ResolvePersistentNotification", MissingAccountError, nil, "", http.StatusNotFound).Wrap(nErr)
default:
return model.NewAppError("ResolvePersistentNotification", "app.user.get.app_error", nil, "", http.StatusInternalServerError).Wrap(nErr)
}
}
if user.IsGuest() {
return nil
}
}
stopNotifications := false
if err := a.forEachPersistentNotificationPost([]*model.Post{post}, func(_ *model.Post, _ *model.Channel, _ *model.Team, mentions *ExplicitMentions, _ model.UserMap, _ map[string]map[string]model.StringMap) error {
if mentions.isUserMentioned(loggedInUserID) {
stopNotifications = true
}
return nil
}); err != nil {
return model.NewAppError("ResolvePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
// Only mentioned users can stop the notifications
if !stopNotifications {
return nil
}
if err := a.Srv().Store().PostPersistentNotification().Delete([]string{post.Id}); err != nil {
return model.NewAppError("ResolvePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
// DeletePersistentNotification stops the persistent notifications.
func (a *App) DeletePersistentNotification(c request.CTX, post *model.Post) *model.AppError {
if !a.IsPersistentNotificationsEnabled() {
return nil
}
_, err := a.Srv().Store().PostPersistentNotification().GetSingle(post.Id)
if err != nil {
var nfErr *store.ErrNotFound
switch {
case errors.As(err, &nfErr):
// Either the notification post is already deleted or was never a notification post
return nil
default:
return model.NewAppError("DeletePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
}
if err := a.Srv().Store().PostPersistentNotification().Delete([]string{post.Id}); err != nil {
return model.NewAppError("DeletePersistentNotification", "app.post_priority.delete_persistent_notification_post.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return nil
}
func (a *App) SendPersistentNotifications() error {
notificationInterval := time.Duration(*a.Config().ServiceSettings.PersistentNotificationIntervalMinutes) * time.Minute
notificationMaxCount := int16(*a.Config().ServiceSettings.PersistentNotificationMaxCount)
// fetch posts for which the "notificationInterval duration" has passed
maxTime := time.Now().Add(-notificationInterval).UnixMilli()
// Pagination loop
for {
notificationPosts, err := a.Srv().Store().PostPersistentNotification().Get(model.GetPersistentNotificationsPostsParams{
MaxTime: maxTime,
MaxSentCount: notificationMaxCount,
PerPage: 500,
})
if err != nil {
return errors.Wrap(err, "failed to get posts for persistent notifications")
}
// No posts left to send persistent notifications
if len(notificationPosts) == 0 {
break
}
postIds := make([]string, 0, len(notificationPosts))
for _, p := range notificationPosts {
postIds = append(postIds, p.PostId)
}
posts, err := a.Srv().Store().Post().GetPostsByIds(postIds)
if err != nil {
return errors.Wrap(err, "failed to get posts by IDs")
}
// Send notifications
if err := a.forEachPersistentNotificationPost(posts, a.sendPersistentNotifications); err != nil {
return err
}
if err := a.Srv().Store().PostPersistentNotification().UpdateLastActivity(postIds); err != nil {
return errors.Wrapf(err, "failed to update lastActivity for notifications: %v", postIds)
}
}
if err := a.Srv().Store().PostPersistentNotification().DeleteExpired(notificationMaxCount); err != nil {
return errors.Wrap(err, "failed to delete expired notifications")
}
return nil
}
func (a *App) forEachPersistentNotificationPost(posts []*model.Post, fn func(post *model.Post, channel *model.Channel, team *model.Team, mentions *ExplicitMentions, profileMap model.UserMap, channelNotifyProps map[string]map[string]model.StringMap) error) error {
channelsMap, teamsMap, err := a.channelTeamMapsForPosts(posts)
if err != nil {
return err
}
channelGroupMap, channelProfileMap, channelKeywords, channelNotifyProps, err := a.persistentNotificationsAuxiliaryData(channelsMap, teamsMap)
if err != nil {
return err
}
for _, post := range posts {
channel := channelsMap[post.ChannelId]
team := teamsMap[channel.TeamId]
// GMs and DMs don't belong to any team
if channel.IsGroupOrDirect() {
team = &model.Team{}
}
profileMap := channelProfileMap[channel.Id]
mentions := &ExplicitMentions{}
// In DMs, only the "other" user can be mentioned
if channel.Type == model.ChannelTypeDirect {
otherUserId := channel.GetOtherUserIdForDM(post.UserId)
if _, ok := profileMap[otherUserId]; ok {
mentions.addMention(otherUserId, DMMention)
}
} else {
keywords := channelKeywords[channel.Id]
mentions = getExplicitMentions(post, keywords, channelGroupMap[channel.Id])
for _, group := range mentions.GroupMentions {
_, err := a.insertGroupMentions(group, channel, profileMap, mentions)
if err != nil {
return errors.Wrapf(err, "failed to include mentions from group - %s for channel - %s", group.Id, channel.Id)
}
}
}
if err := fn(post, channel, team, mentions, profileMap, channelNotifyProps); err != nil {
return err
}
}
return nil
}
func (a *App) persistentNotificationsAuxiliaryData(channelsMap map[string]*model.Channel, teamsMap map[string]*model.Team) (map[string]map[string]*model.Group, map[string]model.UserMap, map[string]map[string][]string, map[string]map[string]model.StringMap, error) {
channelGroupMap := make(map[string]map[string]*model.Group, len(channelsMap))
channelProfileMap := make(map[string]model.UserMap, len(channelsMap))
channelKeywords := make(map[string]map[string][]string, len(channelsMap))
channelNotifyProps := make(map[string]map[string]model.StringMap, len(channelsMap))
for _, c := range channelsMap {
// In DM, notifications can't be send to any 3rd person.
if c.Type != model.ChannelTypeDirect {
groups, err := a.getGroupsAllowedForReferenceInChannel(c, teamsMap[c.TeamId])
if err != nil {
return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
}
channelGroupMap[c.Id] = make(map[string]*model.Group, len(groups))
for k, v := range groups {
channelGroupMap[c.Id][k] = v
}
props, err := a.Srv().Store().Channel().GetAllChannelMembersNotifyPropsForChannel(c.Id, true)
if err != nil {
return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
}
channelNotifyProps[c.Id] = props
}
profileMap, err := a.Srv().Store().User().GetAllProfilesInChannel(context.Background(), c.Id, true)
if err != nil {
return nil, nil, nil, nil, errors.Wrapf(err, "failed to get profiles for channel %s", c.Id)
}
channelKeywords[c.Id] = make(map[string][]string, len(profileMap))
validProfileMap := make(map[string]*model.User, len(profileMap))
for k, v := range profileMap {
if v.IsBot {
continue
}
validProfileMap[k] = v
channelKeywords[c.Id]["@"+v.Username] = []string{k}
}
channelProfileMap[c.Id] = validProfileMap
}
return channelGroupMap, channelProfileMap, channelKeywords, channelNotifyProps, nil
}
func (a *App) channelTeamMapsForPosts(posts []*model.Post) (map[string]*model.Channel, map[string]*model.Team, error) {
channelIds := make(model.StringSet)
for _, p := range posts {
channelIds.Add(p.ChannelId)
}
channels, err := a.Srv().Store().Channel().GetChannelsByIds(channelIds.Val(), false)
if err != nil {
return nil, nil, errors.Wrap(err, "failed to get teams by IDs")
}
channelsMap := make(map[string]*model.Channel, len(channels))
for _, c := range channels {
channelsMap[c.Id] = c
}
teamIds := make(model.StringSet)
for _, c := range channels {
if c.TeamId != "" {
teamIds.Add(c.TeamId)
}
}
teams := make([]*model.Team, 0, len(teamIds))
if len(teamIds) > 0 {
teams, err = a.Srv().Store().Team().GetMany(teamIds.Val())
if err != nil {
return nil, nil, errors.Wrap(err, "failed to get teams by IDs")
}
}
teamsMap := make(map[string]*model.Team, len(teams))
for _, t := range teams {
teamsMap[t.Id] = t
}
return channelsMap, teamsMap, nil
}
func (a *App) sendPersistentNotifications(post *model.Post, channel *model.Channel, team *model.Team, mentions *ExplicitMentions, profileMap model.UserMap, channelNotifyProps map[string]map[string]model.StringMap) error {
mentionedUsersList := make(model.StringArray, 0, len(mentions.Mentions))
for id := range mentions.Mentions {
// Don't send notification to post owner
if id != post.UserId {
mentionedUsersList = append(mentionedUsersList, id)
}
}
sender := profileMap[post.UserId]
notification := &PostNotification{
Post: post,
Channel: channel,
ProfileMap: profileMap,
Sender: sender,
}
if int64(len(mentionedUsersList)) > *a.Config().TeamSettings.MaxNotificationsPerChannel {
return errors.Errorf("mentioned users: %d are more than allowed users: %d", len(mentionedUsersList), *a.Config().TeamSettings.MaxNotificationsPerChannel)
}
if a.canSendPushNotifications() {
for _, userID := range mentionedUsersList {
user := profileMap[userID]
if user == nil {
continue
}
status, err := a.GetStatus(userID)
if err != nil {
mlog.Warn("Unable to fetch online status", mlog.String("user_id", userID), mlog.Err(err))
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
if ShouldSendPushNotification(profileMap[userID], channelNotifyProps[channel.Id][userID], true, status, post) {
a.sendPushNotification(
notification,
user,
true,
false,
"",
)
} else {
// register that a notification was not sent
a.NotificationsLog().Debug("Persistent Notification not sent",
mlog.String("ackId", ""),
mlog.String("type", model.PushTypeMessage),
mlog.String("userId", userID),
mlog.String("postId", post.Id),
mlog.String("status", model.PushNotSent),
)
}
}
}
desktopUsers := make([]string, 0, len(mentionedUsersList))
for _, id := range mentionedUsersList {
user := profileMap[id]
if user == nil {
continue
}
if user.NotifyProps[model.DesktopNotifyProp] != model.UserNotifyNone && a.persistentNotificationsAllowedForStatus(id) {
desktopUsers = append(desktopUsers, id)
}
}
if len(desktopUsers) != 0 {
post = a.PreparePostForClient(request.EmptyContext(a.Log()), post, false, false, true)
postJSON, jsonErr := post.ToJSON()
if jsonErr != nil {
return errors.Wrapf(jsonErr, "failed to encode post to JSON")
}
for _, u := range desktopUsers {
message := model.NewWebSocketEvent(model.WebsocketEventPersistentNotificationTriggered, team.Id, post.ChannelId, u, nil, "")
message.Add("post", postJSON)
message.Add("channel_type", channel.Type)
message.Add("channel_display_name", notification.GetChannelName(model.ShowUsername, ""))
message.Add("channel_name", channel.Name)
message.Add("sender_name", notification.GetSenderName(model.ShowUsername, *a.Config().ServiceSettings.EnablePostUsernameOverride))
message.Add("team_id", team.Id)
if len(post.FileIds) != 0 {
message.Add("otherFile", "true")
infos, err := a.Srv().Store().FileInfo().GetForPost(post.Id, false, false, true)
if err != nil {
mlog.Warn("Unable to get fileInfo for push notifications.", mlog.String("post_id", post.Id), mlog.Err(err))
}
for _, info := range infos {
if info.IsImage() {
message.Add("image", "true")
break
}
}
}
message.Add("mentions", model.ArrayToJSON(desktopUsers))
a.Publish(message)
}
}
return nil
}
func (a *App) persistentNotificationsAllowedForStatus(userID string) bool {
var status *model.Status
var err *model.AppError
if status, err = a.GetStatus(userID); err != nil {
status = &model.Status{UserId: userID, Status: model.StatusOffline, Manual: false, LastActivityAt: 0, ActiveChannel: ""}
}
return status.Status != model.StatusDnd && status.Status != model.StatusOutOfOffice
}
func (a *App) IsPersistentNotificationsEnabled() bool {
return a.IsPostPriorityEnabled() && *a.Config().ServiceSettings.AllowPersistentNotifications
}

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

@@ -0,0 +1,237 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package app
import (
"os"
"testing"
"github.com/mattermost/mattermost-server/server/public/model"
"github.com/mattermost/mattermost-server/server/v8/channels/store"
storemocks "github.com/mattermost/mattermost-server/server/v8/channels/store/storetest/mocks"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
func TestResolvePersistentNotification(t *testing.T) {
t.Run("should not delete when no posts exist", func(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY")
th := SetupWithStoreMock(t)
defer th.TearDown()
post := &model.Post{Id: "test id"}
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
mockPostPersistentNotification.On("GetSingle", mock.Anything).Return(nil, &store.ErrNotFound{})
mockPostPersistentNotification.On("Delete", mock.Anything).Return(nil)
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
cfg := th.App.Config()
*cfg.ServiceSettings.PostPriority = true
*cfg.ServiceSettings.AllowPersistentNotificationsForGuests = true
err := th.App.ResolvePersistentNotification(th.Context, post, "")
require.Nil(t, err)
mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
})
t.Run("should delete for mentioned user", func(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY")
th := SetupWithStoreMock(t)
defer th.TearDown()
user1 := &model.User{Id: "uid1", Username: "user-1"}
user2 := &model.User{Id: "uid2", Username: "user-2"}
profileMap := map[string]*model.User{user1.Id: user1, user2.Id: user2}
team := &model.Team{Id: "tid"}
channel := &model.Channel{Id: "chid", TeamId: team.Id, Type: model.ChannelTypeOpen}
post := &model.Post{Id: "pid", ChannelId: channel.Id, Message: "tagging @" + user1.Username, UserId: user2.Id}
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
mockPostPersistentNotification.On("GetSingle", mock.Anything).Return(&model.PostPersistentNotifications{PostId: post.Id}, nil)
mockPostPersistentNotification.On("Delete", mock.Anything).Return(nil)
mockChannel := storemocks.ChannelStore{}
mockStore.On("Channel").Return(&mockChannel)
mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{channel}, nil)
mockChannel.On("GetAllChannelMembersNotifyPropsForChannel", mock.Anything, mock.Anything).Return(map[string]model.StringMap{}, nil)
mockTeam := storemocks.TeamStore{}
mockStore.On("Team").Return(&mockTeam)
mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{team}, nil)
mockUser := storemocks.UserStore{}
mockStore.On("User").Return(&mockUser)
mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
mockGroup := storemocks.GroupStore{}
mockStore.On("Group").Return(&mockGroup)
mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
cfg := th.App.Config()
*cfg.ServiceSettings.PostPriority = true
*cfg.ServiceSettings.AllowPersistentNotificationsForGuests = true
err := th.App.ResolvePersistentNotification(th.Context, post, user1.Id)
require.Nil(t, err)
mockPostPersistentNotification.AssertCalled(t, "Delete", mock.Anything)
})
t.Run("should not delete for post owner", func(t *testing.T) {
th := SetupWithStoreMock(t)
defer th.TearDown()
user1 := &model.User{Id: "uid1"}
post := &model.Post{Id: "test id", UserId: user1.Id}
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
err := th.App.ResolvePersistentNotification(th.Context, post, user1.Id)
require.Nil(t, err)
mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
})
t.Run("should not delete for non-mentioned user", func(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY")
th := SetupWithStoreMock(t)
defer th.TearDown()
user1 := &model.User{Id: "uid1", Username: "user-1"}
user2 := &model.User{Id: "uid2", Username: "user-2"}
user3 := &model.User{Id: "uid3", Username: "user-3"}
profileMap := map[string]*model.User{user1.Id: user1, user2.Id: user2, user3.Id: user3}
team := &model.Team{Id: "tid"}
channel := &model.Channel{Id: "chid", TeamId: team.Id, Type: model.ChannelTypeOpen}
post := &model.Post{Id: "pid", ChannelId: channel.Id, Message: "tagging @" + user1.Username, UserId: user2.Id}
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
mockPostPersistentNotification.On("GetSingle", mock.Anything).Return(&model.PostPersistentNotifications{PostId: post.Id}, nil)
mockPostPersistentNotification.On("Delete", mock.Anything).Return(nil)
mockChannel := storemocks.ChannelStore{}
mockStore.On("Channel").Return(&mockChannel)
mockChannel.On("GetChannelsByIds", mock.Anything, mock.Anything).Return([]*model.Channel{channel}, nil)
mockChannel.On("GetAllChannelMembersNotifyPropsForChannel", mock.Anything, mock.Anything).Return(map[string]model.StringMap{}, nil)
mockTeam := storemocks.TeamStore{}
mockStore.On("Team").Return(&mockTeam)
mockTeam.On("GetMany", mock.Anything).Return([]*model.Team{team}, nil)
mockUser := storemocks.UserStore{}
mockStore.On("User").Return(&mockUser)
mockUser.On("GetAllProfilesInChannel", mock.Anything, mock.Anything, mock.Anything).Return(profileMap, nil)
mockGroup := storemocks.GroupStore{}
mockStore.On("Group").Return(&mockGroup)
mockGroup.On("GetGroups", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]*model.Group{}, nil)
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
cfg := th.App.Config()
*cfg.ServiceSettings.PostPriority = true
*cfg.ServiceSettings.AllowPersistentNotificationsForGuests = true
err := th.App.ResolvePersistentNotification(th.Context, post, user3.Id)
require.Nil(t, err)
mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
})
}
func TestDeletePersistentNotification(t *testing.T) {
t.Run("should not delete when no posts exist", func(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY")
th := SetupWithStoreMock(t)
defer th.TearDown()
post := &model.Post{Id: "test id"}
mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
mockPostPersistentNotification.On("GetSingle", mock.Anything).Return(nil, &store.ErrNotFound{})
mockPostPersistentNotification.On("Delete", mock.Anything).Return(nil)
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
cfg := th.App.Config()
*cfg.ServiceSettings.PostPriority = true
*cfg.ServiceSettings.AllowPersistentNotificationsForGuests = true
err := th.App.DeletePersistentNotification(th.Context, post)
require.Nil(t, err)
mockPostPersistentNotification.AssertNotCalled(t, "Delete", mock.Anything)
})
t.Run("should delete", func(t *testing.T) {
os.Setenv("MM_FEATUREFLAGS_POSTPRIORITY", "true")
defer os.Unsetenv("MM_FEATUREFLAGS_POSTPRIORITY")
th := SetupWithStoreMock(t)
defer th.TearDown()
post := &model.Post{Id: "test id"}
mockPostPersistentNotification := storemocks.PostPersistentNotificationStore{}
mockPostPersistentNotification.On("GetSingle", mock.Anything).Return(&model.PostPersistentNotifications{PostId: post.Id}, nil)
mockPostPersistentNotification.On("Delete", mock.Anything).Return(nil)
mockStore := th.App.Srv().Store().(*storemocks.Store)
mockStore.On("PostPersistentNotification").Return(&mockPostPersistentNotification)
th.App.Srv().SetLicense(getLicWithSkuShortName(model.LicenseShortSkuProfessional))
cfg := th.App.Config()
*cfg.ServiceSettings.PostPriority = true
err := th.App.DeletePersistentNotification(th.Context, post)
require.Nil(t, err)
mockPostPersistentNotification.AssertCalled(t, "Delete", mock.Anything)
})
}
func TestSendPersistentNotifications(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.App.AddUserToChannel(th.Context, th.BasicUser2, th.BasicChannel, false)
s := "Urgent"
tr := true
p1 := &model.Post{
UserId: th.BasicUser.Id,
ChannelId: th.BasicChannel.Id,
Message: "test " + "@" + th.BasicUser2.Username,
Metadata: &model.PostMetadata{
Priority: &model.PostPriority{
Priority: &s,
PersistentNotifications: &tr,
},
},
}
_, appErr := th.App.CreatePost(th.Context, p1, th.BasicChannel, false, false)
require.Nil(t, appErr)
err := th.App.SendPersistentNotifications()
require.NoError(t, err)
}

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

@@ -32,3 +32,7 @@ func (a *App) GetPriorityForPostList(list *model.PostList) (map[string]*model.Po
return priorityMap, nil
}
func (a *App) IsPostPriorityEnabled() bool {
return a.Config().FeatureFlags.PostPriority && *a.Config().ServiceSettings.PostPriority
}

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

@@ -26,7 +26,7 @@ func (a *App) SaveReactionForPost(c *request.Context, reaction *model.Reaction)
}
if channel.DeleteAt > 0 {
return nil, model.NewAppError("deleteReactionForPost", "api.reaction.save.archived_channel.app_error", nil, "", http.StatusForbidden)
return nil, model.NewAppError("SaveReactionForPost", "api.reaction.save.archived_channel.app_error", nil, "", http.StatusForbidden)
}
reaction, nErr := a.Srv().Store().Reaction().Save(reaction)
@@ -40,6 +40,12 @@ func (a *App) SaveReactionForPost(c *request.Context, reaction *model.Reaction)
}
}
if post.RootId == "" {
if appErr := a.ResolvePersistentNotification(c, post, reaction.UserId); appErr != nil {
return nil, appErr
}
}
// The post is always modified since the UpdateAt always changes
a.invalidateCacheForChannelPosts(post.ChannelId)

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

@@ -51,6 +51,7 @@ import (
"github.com/mattermost/mattermost-server/server/v8/channels/jobs/migrations"
"github.com/mattermost/mattermost-server/server/v8/channels/jobs/notify_admin"
"github.com/mattermost/mattermost-server/server/v8/channels/jobs/plugins"
"github.com/mattermost/mattermost-server/server/v8/channels/jobs/post_persistent_notifications"
"github.com/mattermost/mattermost-server/server/v8/channels/jobs/product_notices"
"github.com/mattermost/mattermost-server/server/v8/channels/jobs/resend_invitation_email"
"github.com/mattermost/mattermost-server/server/v8/channels/product"
@@ -1582,6 +1583,12 @@ func (s *Server) initJobs() {
notify_admin.MakeScheduler(s.Jobs, s.License(), model.JobTypeTrialNotifyAdmin),
)
s.Jobs.RegisterJobType(
model.JobTypePostPersistentNotifications,
post_persistent_notifications.MakeWorker(s.Jobs, New(ServerConnector(s.Channels()))),
post_persistent_notifications.MakeScheduler(s.Jobs, func() *model.License { return s.License() }),
)
s.Jobs.RegisterJobType(
model.JobTypeInstallPluginNotifyAdmin,
notify_admin.MakeInstallPluginNotifyWorker(s.Jobs, New(ServerConnector(s.Channels()))),

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

@@ -1897,7 +1897,7 @@ func (a *App) GetTeamsUnreadForUser(excludeTeamId string, userID string, include
includeCollapsedThreads = includeCollapsedThreads && *a.Config().ServiceSettings.CollapsedThreads != model.CollapsedThreadsDisabled
if includeCollapsedThreads {
teamUnreads, err := a.Srv().Store().Thread().GetTeamsUnreadForUser(userID, teamIDs, a.isPostPriorityEnabled())
teamUnreads, err := a.Srv().Store().Thread().GetTeamsUnreadForUser(userID, teamIDs, a.IsPostPriorityEnabled())
if err != nil {
return nil, model.NewAppError("GetTeamsUnreadForUser", "app.team.get_unread.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
@@ -1976,6 +1976,10 @@ func (a *App) SoftDeleteTeam(teamID string) *model.AppError {
return err
}
if err := a.Srv().Store().PostPersistentNotification().DeleteByTeam([]string{team.Id}); err != nil {
return model.NewAppError("SoftDeleteTeam", "app.post_persistent_notification.delete_by_team.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
team.DeleteAt = model.GetMillis()
team, nErr := a.Srv().Store().Team().Update(team)
if nErr != nil {

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

@@ -2474,7 +2474,7 @@ func (a *App) ConvertBotToUser(c request.CTX, bot *model.Bot, userPatch *model.U
func (a *App) GetThreadsForUser(userID, teamID string, options model.GetUserThreadsOpts) (*model.Threads, *model.AppError) {
var result model.Threads
var eg errgroup.Group
postPriorityIsEnabled := a.isPostPriorityEnabled()
postPriorityIsEnabled := a.IsPostPriorityEnabled()
if postPriorityIsEnabled {
options.IncludeIsUrgent = true
}
@@ -2571,7 +2571,7 @@ func (a *App) GetThreadMembershipForUser(userId, threadId string) (*model.Thread
}
func (a *App) GetThreadForUser(threadMembership *model.ThreadMembership, extended bool) (*model.ThreadResponse, *model.AppError) {
thread, nErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, extended, a.isPostPriorityEnabled())
thread, nErr := a.Srv().Store().Thread().GetThreadForUser(threadMembership, extended, a.IsPostPriorityEnabled())
if nErr != nil {
var nfErr *store.ErrNotFound
switch {
@@ -2657,7 +2657,7 @@ func (a *App) UpdateThreadFollowForUserFromChannelAdd(c request.CTX, userID, tea
}
message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, teamID, "", userID, nil, "")
userThread, err := a.Srv().Store().Thread().GetThreadForUser(tm, true, a.isPostPriorityEnabled())
userThread, err := a.Srv().Store().Thread().GetThreadForUser(tm, true, a.IsPostPriorityEnabled())
if err != nil {
var errNotFound *store.ErrNotFound