diff --git a/api4/channel.go b/api4/channel.go index 3aaa96aafe..5bfbddf460 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -1698,6 +1698,14 @@ func addChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { return } + if postRootId != "" { + err := c.App.UpdateThreadFollowForUserFromChannelAdd(cm.UserId, channel.TeamId, postRootId) + if err != nil { + c.Err = err + return + } + } + auditRec.Success() auditRec.AddMeta("add_user_id", cm.UserId) c.LogAudit("name=" + channel.Name + " user_id=" + cm.UserId) diff --git a/api4/channel_test.go b/api4/channel_test.go index 5741615bf5..45f140a7bc 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -3012,6 +3012,63 @@ func TestAddChannelMember(t *testing.T) { }) } +func TestAddChannelMemberFromThread(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + team := th.BasicTeam + user := th.BasicUser + user2 := th.BasicUser2 + user3 := th.CreateUserWithClient(th.SystemAdminClient) + _, _, err := th.SystemAdminClient.AddTeamMember(team.Id, user3.Id) + require.NoError(t, err) + + publicChannel := th.CreatePublicChannel() + + _, resp, err := th.Client.AddChannelMember(publicChannel.Id, user.Id) + require.NoError(t, err) + CheckCreatedStatus(t, resp) + _, resp, err = th.Client.AddChannelMember(publicChannel.Id, user2.Id) + require.NoError(t, err) + CheckCreatedStatus(t, resp) + + post := &model.Post{ + ChannelId: publicChannel.Id, + Message: "A root post", + UserId: user.Id, + } + rpost, _, err := th.SystemAdminClient.CreatePost(post) + require.NoError(t, err) + + _, _, err = th.SystemAdminClient.CreatePost( + &model.Post{ + ChannelId: publicChannel.Id, + Message: "A reply post with mention @" + user3.Username, + UserId: user2.Id, + RootId: rpost.Id, + }) + require.NoError(t, err) + + _, _, err = th.SystemAdminClient.CreatePost( + &model.Post{ + ChannelId: publicChannel.Id, + Message: "Another reply post with mention @" + user3.Username, + UserId: user2.Id, + RootId: rpost.Id, + }) + require.NoError(t, err) + + // Simulate adding a user to a channel from a thread + _, _, err = th.SystemAdminClient.AddChannelMemberWithRootId(publicChannel.Id, user3.Id, rpost.Id) + require.NoError(t, err) + + // Threadmembership should exist for added user + ut, _, err := th.SystemAdminClient.GetUserThread(user3.Id, team.Id, rpost.Id, false) + require.NoError(t, err) + // Should have two mentions. There might be a race condition + // here between the "added user to the channel" message and the GetUserThread call + require.LessOrEqual(t, int64(2), ut.UnreadMentions) +} + func TestAddChannelMemberAddMyself(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/app/app_iface.go b/app/app_iface.go index 108934b0f8..272358df7d 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -1063,6 +1063,7 @@ type AppIface interface { UpdateTeamPrivacy(teamID string, teamType string, allowOpenInvite bool) *model.AppError UpdateTeamScheme(team *model.Team) (*model.Team, *model.AppError) UpdateThreadFollowForUser(userID, teamID, threadID string, state bool) *model.AppError + UpdateThreadFollowForUserFromChannelAdd(userID, teamID, threadID string) *model.AppError UpdateThreadReadForUser(currentSessionId, userID, teamID, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) UpdateThreadsReadForUser(userID, teamID string) *model.AppError UpdateUser(user *model.User, sendNotifications bool) (*model.User, *model.AppError) diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index 695bce2a25..367a6af60b 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -16634,6 +16634,28 @@ func (a *OpenTracingAppLayer) UpdateThreadFollowForUser(userID string, teamID st return resultVar0 } +func (a *OpenTracingAppLayer) UpdateThreadFollowForUserFromChannelAdd(userID string, teamID string, threadID string) *model.AppError { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadFollowForUserFromChannelAdd") + + 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.UpdateThreadFollowForUserFromChannelAdd(userID, teamID, threadID) + + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (a *OpenTracingAppLayer) UpdateThreadReadForUser(currentSessionId string, userID string, teamID string, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateThreadReadForUser") diff --git a/app/user.go b/app/user.go index 34dab0b6dd..1cb5396234 100644 --- a/app/user.go +++ b/app/user.go @@ -2301,6 +2301,64 @@ func (a *App) UpdateThreadFollowForUser(userID, teamID, threadID string, state b return nil } +func (a *App) UpdateThreadFollowForUserFromChannelAdd(userID, teamID, threadID string) *model.AppError { + opts := store.ThreadMembershipOpts{ + Following: true, + IncrementMentions: false, + UpdateFollowing: true, + UpdateViewedTimestamp: false, + UpdateParticipants: false, + } + tm, err := a.Srv().Store.Thread().MaintainMembership(userID, threadID, opts) + if err != nil { + return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + post, appErr := a.GetSinglePost(threadID) + if appErr != nil { + return appErr + } + user, appErr := a.GetUser(userID) + if appErr != nil { + return appErr + } + tm.UnreadMentions, appErr = a.countThreadMentions(user, post, teamID, post.CreateAt-1) + if appErr != nil { + return appErr + } + tm.LastViewed = post.CreateAt - 1 + _, err = a.Srv().Store.Thread().UpdateMembership(tm) + if err != nil { + return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, err.Error(), http.StatusInternalServerError) + } + + message := model.NewWebSocketEvent(model.WebsocketEventThreadUpdated, teamID, "", userID, nil) + userThread, err := a.Srv().Store.Thread().GetThreadForUser(teamID, tm, true) + if err != nil { + var errNotFound *store.ErrNotFound + if errors.As(err, &errNotFound) { + return nil + } + return model.NewAppError("UpdateThreadFollowForUserFromChannelAdd", "app.user.update_thread_follow_for_user.app_error", nil, err.Error(), http.StatusInternalServerError) + } + a.sanitizeProfiles(userThread.Participants, false) + userThread.Post.SanitizeProps() + sanitizedPost, appErr := a.SanitizePostMetadataForUser(userThread.Post, userID) + if appErr != nil { + return appErr + } + userThread.Post = sanitizedPost + + payload, jsonErr := json.Marshal(userThread) + if jsonErr != nil { + mlog.Warn("Failed to encode thread to JSON") + } + message.Add("thread", string(payload)) + + a.Publish(message) + return nil +} + func (a *App) UpdateThreadReadForUser(currentSessionId, userID, teamID, threadID string, timestamp int64) (*model.ThreadResponse, *model.AppError) { user, err := a.GetUser(userID) if err != nil { diff --git a/store/sqlstore/thread_store.go b/store/sqlstore/thread_store.go index 94d0aaaa8f..2ccce96e4b 100644 --- a/store/sqlstore/thread_store.go +++ b/store/sqlstore/thread_store.go @@ -425,6 +425,9 @@ func (s *SqlThreadStore) GetThreadForUser(teamId string, threadMembership *model err := s.GetReplica().SelectOne(&thread, query, args...) if err != nil { + if err == sql.ErrNoRows { + return nil, store.NewErrNotFound("Thread", threadMembership.PostId) + } return nil, err }