From 2ebc8ec90ffefb366d1cf48e06ae426bb6e53352 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Mon, 16 Nov 2020 15:19:01 -0500 Subject: [PATCH] MM-20897 Add category muting (#16225) * Make UpdateSidebarCategories return the original categories * MM-20897 Add category muting * Prevent muting the DMs category * Fix muted state not being stored in the database * Address feedback * Address some feedback * Fix unit tests * MM-20897 Mute/unmute channels in the database in bulk * Satisfy golangci-lint --- api4/channel_category_test.go | 86 ++++ app/app_iface.go | 2 +- app/channel.go | 143 ++++-- app/channel_category.go | 112 ++++- app/channel_category_test.go | 521 ++++++++++++++++++++ app/channel_test.go | 51 ++ app/opentracing/opentracing_layer.go | 11 +- app/slashcommands/command_mute.go | 17 +- model/channel_member.go | 12 + model/channel_sidebar.go | 1 + store/opentracinglayer/opentracinglayer.go | 24 +- store/retrylayer/retrylayer.go | 30 +- store/sqlstore/channel_store.go | 27 +- store/sqlstore/channel_store_categories.go | 37 +- store/sqlstore/upgrade.go | 21 +- store/store.go | 3 +- store/storetest/channel_store.go | 59 +++ store/storetest/channel_store_categories.go | 115 ++++- store/storetest/mocks/ChannelStore.go | 42 +- store/timerlayer/timerlayer.go | 22 +- 20 files changed, 1189 insertions(+), 147 deletions(-) diff --git a/api4/channel_category_test.go b/api4/channel_category_test.go index 6078ee5bff..1e2a2cba7a 100644 --- a/api4/channel_category_test.go +++ b/api4/channel_category_test.go @@ -251,6 +251,92 @@ func TestUpdateCategoryForTeamForUser(t *testing.T) { assert.NotContains(t, received.Channels, channel.Id) assert.Equal(t, channelsCategory.Channels, received.Channels) }) + + t.Run("muting a category should mute all of its channels", func(t *testing.T) { + user, client := setupUserForSubtest(t, th) + + categories, resp := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "") + require.Nil(t, resp.Error) + require.Len(t, categories.Categories, 3) + require.Len(t, categories.Order, 3) + + channelsCategory := categories.Categories[1] + require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) + require.True(t, len(channelsCategory.Channels) > 0) + + // Mute the category + updatedCategory := &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + Id: channelsCategory.Id, + UserId: user.Id, + TeamId: th.BasicTeam.Id, + Sorting: channelsCategory.Sorting, + Muted: true, + }, + Channels: channelsCategory.Channels, + } + + received, resp := client.UpdateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, channelsCategory.Id, updatedCategory) + require.Nil(t, resp.Error) + assert.Equal(t, channelsCategory.Id, received.Id) + assert.True(t, received.Muted) + + // Check that the muted category was saved in the database + received, resp = client.GetSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, channelsCategory.Id, "") + require.Nil(t, resp.Error) + assert.Equal(t, channelsCategory.Id, received.Id) + assert.True(t, received.Muted) + + // Confirm that the channels in the category were muted + member, resp := client.GetChannelMember(channelsCategory.Channels[0], user.Id, "") + require.Nil(t, resp.Error) + assert.True(t, member.IsChannelMuted()) + }) + + t.Run("should not be able to mute DM category", func(t *testing.T) { + user, client := setupUserForSubtest(t, th) + + categories, resp := client.GetSidebarCategoriesForTeamForUser(user.Id, th.BasicTeam.Id, "") + require.Nil(t, resp.Error) + require.Len(t, categories.Categories, 3) + require.Len(t, categories.Order, 3) + + dmsCategory := categories.Categories[2] + require.Equal(t, model.SidebarCategoryDirectMessages, dmsCategory.Type) + require.Len(t, dmsCategory.Channels, 0) + + // Ensure a DM channel exists + dmChannel, resp := client.CreateDirectChannel(user.Id, th.BasicUser.Id) + require.Nil(t, resp.Error) + + // Attempt to mute the category + updatedCategory := &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + Id: dmsCategory.Id, + UserId: user.Id, + TeamId: th.BasicTeam.Id, + Sorting: dmsCategory.Sorting, + Muted: true, + }, + Channels: []string{dmChannel.Id}, + } + + received, resp := client.UpdateSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, dmsCategory.Id, updatedCategory) + require.Nil(t, resp.Error) + assert.Equal(t, dmsCategory.Id, received.Id) + assert.False(t, received.Muted) + + // Check that the muted category was not saved in the database + received, resp = client.GetSidebarCategoryForTeamForUser(user.Id, th.BasicTeam.Id, dmsCategory.Id, "") + require.Nil(t, resp.Error) + assert.Equal(t, dmsCategory.Id, received.Id) + assert.False(t, received.Muted) + + // Confirm that the channels in the category were not muted + member, resp := client.GetChannelMember(dmChannel.Id, user.Id, "") + require.Nil(t, resp.Error) + assert.False(t, member.IsChannelMuted()) + }) } func TestUpdateCategoriesForTeamForUser(t *testing.T) { diff --git a/app/app_iface.go b/app/app_iface.go index 72351b70ba..617d8bd1e9 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -953,7 +953,7 @@ type AppIface interface { TestLdap() *model.AppError TestSiteURL(siteURL string) *model.AppError Timezones() *timezones.Timezones - ToggleMuteChannel(channelId string, userId string) *model.ChannelMember + ToggleMuteChannel(channelId, userId string) (*model.ChannelMember, *model.AppError) TotalWebsocketConnections() int TriggerWebhook(payload *model.OutgoingWebhookPayload, hook *model.OutgoingWebhook, post *model.Post, channel *model.Channel) UnregisterPluginCommand(pluginId, teamId, trigger string) diff --git a/app/channel.go b/app/channel.go index e8526a3550..8c12a68bf4 100644 --- a/app/channel.go +++ b/app/channel.go @@ -1097,22 +1097,7 @@ func (a *App) UpdateChannelMemberRoles(channelId string, userId string, newRoles member.ExplicitRoles = strings.Join(newExplicitRoles, " ") - member, nErr := a.Srv().Store.Channel().UpdateMember(member) - if nErr != nil { - var appErr *model.AppError - var nfErr *store.ErrNotFound - switch { - case errors.As(nErr, &appErr): - return nil, appErr - case errors.As(nErr, &nfErr): - return nil, model.NewAppError("UpdateChannelMemberRoles", MISSING_CHANNEL_MEMBER_ERROR, nil, nfErr.Error(), http.StatusNotFound) - default: - return nil, model.NewAppError("UpdateChannelMemberRoles", "app.channel.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError) - } - } - - a.InvalidateCacheForUser(userId) - return member, nil + return a.updateChannelMember(member) } func (a *App) UpdateChannelMemberSchemeRoles(channelId string, userId string, isSchemeGuest bool, isSchemeUser bool, isSchemeAdmin bool) (*model.ChannelMember, *model.AppError) { @@ -1134,27 +1119,7 @@ func (a *App) UpdateChannelMemberSchemeRoles(channelId string, userId string, is member.ExplicitRoles = RemoveRoles([]string{model.CHANNEL_GUEST_ROLE_ID, model.CHANNEL_USER_ROLE_ID, model.CHANNEL_ADMIN_ROLE_ID}, member.ExplicitRoles) } - member, nErr := a.Srv().Store.Channel().UpdateMember(member) - if nErr != nil { - var appErr *model.AppError - var nfErr *store.ErrNotFound - switch { - case errors.As(nErr, &appErr): - return nil, appErr - case errors.As(nErr, &nfErr): - return nil, model.NewAppError("UpdateChannelMemberSchemeRoles", MISSING_CHANNEL_MEMBER_ERROR, nil, nfErr.Error(), http.StatusNotFound) - default: - return nil, model.NewAppError("UpdateChannelMemberSchemeRoles", "app.channel.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError) - } - } - - // Notify the clients that the member notify props changed - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED, "", "", userId, nil) - message.Add("channelMember", member.ToJson()) - a.Publish(message) - - a.InvalidateCacheForUser(userId) - return member, nil + return a.updateChannelMember(member) } func (a *App) UpdateChannelMemberNotifyProps(data map[string]string, channelId string, userId string) (*model.ChannelMember, *model.AppError) { @@ -1185,6 +1150,17 @@ func (a *App) UpdateChannelMemberNotifyProps(data map[string]string, channelId s member.NotifyProps[model.IGNORE_CHANNEL_MENTIONS_NOTIFY_PROP] = ignoreChannelMentions } + member, err = a.updateChannelMember(member) + if err != nil { + return nil, err + } + + a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId) + + return member, nil +} + +func (a *App) updateChannelMember(member *model.ChannelMember) (*model.ChannelMember, *model.AppError) { member, nErr := a.Srv().Store.Channel().UpdateMember(member) if nErr != nil { var appErr *model.AppError @@ -1193,18 +1169,19 @@ func (a *App) UpdateChannelMemberNotifyProps(data map[string]string, channelId s case errors.As(nErr, &appErr): return nil, appErr case errors.As(nErr, &nfErr): - return nil, model.NewAppError("UpdateChannelMemberNotifyProps", MISSING_CHANNEL_MEMBER_ERROR, nil, nfErr.Error(), http.StatusNotFound) + return nil, model.NewAppError("updateChannelMember", MISSING_CHANNEL_MEMBER_ERROR, nil, nfErr.Error(), http.StatusNotFound) default: - return nil, model.NewAppError("UpdateChannelMemberNotifyProps", "app.channel.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError) + return nil, model.NewAppError("updateChannelMember", "app.channel.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError) } } - a.InvalidateCacheForUser(userId) - a.invalidateCacheForChannelMembersNotifyProps(channelId) + a.InvalidateCacheForUser(member.UserId) + // Notify the clients that the member notify props changed - evt := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED, "", "", userId, nil) + evt := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED, "", "", member.UserId, nil) evt.Add("channelMember", member.ToJson()) a.Publish(evt) + return member, nil } @@ -2788,20 +2765,84 @@ func (a *App) GetPinnedPosts(channelId string) (*model.PostList, *model.AppError return posts, nil } -func (a *App) ToggleMuteChannel(channelId string, userId string) *model.ChannelMember { - member, err := a.Srv().Store.Channel().GetMember(channelId, userId) +func (a *App) ToggleMuteChannel(channelId, userId string) (*model.ChannelMember, *model.AppError) { + member, nErr := a.Srv().Store.Channel().GetMember(channelId, userId) + if nErr != nil { + var appErr *model.AppError + var nfErr *store.ErrNotFound + switch { + case errors.As(nErr, &appErr): + return nil, appErr + case errors.As(nErr, &nfErr): + return nil, model.NewAppError("ToggleMuteChannel", MISSING_CHANNEL_MEMBER_ERROR, nil, nfErr.Error(), http.StatusNotFound) + default: + return nil, model.NewAppError("ToggleMuteChannel", "app.channel.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError) + } + } + + member.SetChannelMuted(!member.IsChannelMuted()) + + member, err := a.updateChannelMember(member) if err != nil { - return nil + return nil, err } - if member.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CHANNEL_NOTIFY_MENTION { - member.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] = model.CHANNEL_MARK_UNREAD_ALL - } else { - member.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] = model.CHANNEL_NOTIFY_MENTION + a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId) + + return member, nil +} + +func (a *App) setChannelsMuted(channelIds []string, userId string, muted bool) ([]*model.ChannelMember, *model.AppError) { + members, nErr := a.Srv().Store.Channel().GetMembersByChannelIds(channelIds, userId) + if nErr != nil { + var appErr *model.AppError + switch { + case errors.As(nErr, &appErr): + return nil, appErr + default: + return nil, model.NewAppError("setChannelsMuted", "app.channel.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError) + } } - a.Srv().Store.Channel().UpdateMember(member) - return member + var membersToUpdate []*model.ChannelMember + for _, member := range *members { + if muted == member.IsChannelMuted() { + continue + } + + updatedMember := member + updatedMember.SetChannelMuted(muted) + + membersToUpdate = append(membersToUpdate, &updatedMember) + } + + if len(membersToUpdate) == 0 { + return nil, nil + } + + updated, nErr := a.Srv().Store.Channel().UpdateMultipleMembers(membersToUpdate) + if nErr != nil { + var appErr *model.AppError + var nfErr *store.ErrNotFound + switch { + case errors.As(nErr, &appErr): + return nil, appErr + case errors.As(nErr, &nfErr): + return nil, model.NewAppError("setChannelsMuted", MISSING_CHANNEL_MEMBER_ERROR, nil, nfErr.Error(), http.StatusNotFound) + default: + return nil, model.NewAppError("setChannelsMuted", "app.channel.get_member.app_error", nil, nErr.Error(), http.StatusInternalServerError) + } + } + + for _, member := range updated { + a.invalidateCacheForChannelMembersNotifyProps(member.ChannelId) + + evt := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED, "", "", member.UserId, nil) + evt.Add("channelMember", member.ToJson()) + a.Publish(evt) + } + + return updated, nil } func (a *App) FillInChannelProps(channel *model.Channel) *model.AppError { diff --git a/app/channel_category.go b/app/channel_category.go index b321e6a3aa..437f6d8e91 100644 --- a/app/channel_category.go +++ b/app/channel_category.go @@ -144,14 +144,122 @@ func (a *App) UpdateSidebarCategoryOrder(userId, teamId string, categoryOrder [] } func (a *App) UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, *model.AppError) { - result, err := a.Srv().Store.Channel().UpdateSidebarCategories(userId, teamId, categories) + updatedCategories, originalCategories, err := a.Srv().Store.Channel().UpdateSidebarCategories(userId, teamId, categories) if err != nil { return nil, model.NewAppError("UpdateSidebarCategories", "app.channel.sidebar_categories.app_error", nil, err.Error(), http.StatusInternalServerError) } message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_SIDEBAR_CATEGORY_UPDATED, teamId, "", userId, nil) a.Publish(message) - return result, nil + + a.muteChannelsForUpdatedCategories(userId, updatedCategories, originalCategories) + + return updatedCategories, nil +} + +func (a *App) muteChannelsForUpdatedCategories(userId string, updatedCategories []*model.SidebarCategoryWithChannels, originalCategories []*model.SidebarCategoryWithChannels) { + var channelsToMute []string + var channelsToUnmute []string + + // Mute or unmute all channels in categories that were muted or unmuted + for i, updatedCategory := range updatedCategories { + if i > len(originalCategories)-1 { + // The two slices should be the same length, but double check that to be safe + continue + } + + originalCategory := originalCategories[i] + + if updatedCategory.Muted && !originalCategory.Muted { + channelsToMute = append(channelsToMute, updatedCategory.Channels...) + } else if !updatedCategory.Muted && originalCategory.Muted { + channelsToUnmute = append(channelsToUnmute, updatedCategory.Channels...) + } + } + + // Mute any channels moved from an unmuted category into a muted one and vice versa + channelsDiff := diffChannelsBetweenCategories(updatedCategories, originalCategories) + if len(channelsDiff) != 0 { + makeCategoryMap := func(categories []*model.SidebarCategoryWithChannels) map[string]*model.SidebarCategoryWithChannels { + result := make(map[string]*model.SidebarCategoryWithChannels) + for _, category := range categories { + result[category.Id] = category + } + + return result + } + + updatedCategoriesById := makeCategoryMap(updatedCategories) + originalCategoriesById := makeCategoryMap(originalCategories) + + for channelId, diff := range channelsDiff { + fromCategory := originalCategoriesById[diff.fromCategoryId] + toCategory := updatedCategoriesById[diff.toCategoryId] + + if toCategory.Muted && !fromCategory.Muted { + channelsToMute = append(channelsToMute, channelId) + } else if !toCategory.Muted && fromCategory.Muted { + channelsToUnmute = append(channelsToUnmute, channelId) + } + } + } + + if len(channelsToMute) > 0 { + _, err := a.setChannelsMuted(channelsToMute, userId, true) + if err != nil { + mlog.Error( + "Failed to mute channels to match category", + mlog.String("user_id", userId), + mlog.Err(err), + ) + } + } + + if len(channelsToUnmute) > 0 { + _, err := a.setChannelsMuted(channelsToUnmute, userId, false) + if err != nil { + mlog.Error( + "Failed to unmute channels to match category", + mlog.String("user_id", userId), + mlog.Err(err), + ) + } + } +} + +type categoryChannelDiff struct { + fromCategoryId string + toCategoryId string +} + +func diffChannelsBetweenCategories(updatedCategories []*model.SidebarCategoryWithChannels, originalCategories []*model.SidebarCategoryWithChannels) map[string]*categoryChannelDiff { + // mapChannelIdsToCategories returns a map of channel IDs to the IDs of the categories that they're a member of. + mapChannelIdsToCategories := func(categories []*model.SidebarCategoryWithChannels) map[string]string { + result := make(map[string]string) + for _, category := range categories { + for _, channelId := range category.Channels { + result[channelId] = category.Id + } + } + + return result + } + + updatedChannelIdsMap := mapChannelIdsToCategories(updatedCategories) + originalChannelIdsMap := mapChannelIdsToCategories(originalCategories) + + // Check for any channels that have changed categories. Note that we don't worry about any channels that have moved + // outside of these categories since that heavily complicates things and doesn't currently happen in our apps. + channelsDiff := make(map[string]*categoryChannelDiff) + for channelId, originalCategoryId := range originalChannelIdsMap { + updatedCategoryId := updatedChannelIdsMap[channelId] + + if originalCategoryId != updatedCategoryId && updatedCategoryId != "" { + channelsDiff[channelId] = &categoryChannelDiff{originalCategoryId, updatedCategoryId} + } + } + + return channelsDiff } func (a *App) DeleteSidebarCategory(userId, teamId, categoryId string) *model.AppError { diff --git a/app/channel_category_test.go b/app/channel_category_test.go index c770e9f824..a6999f852a 100644 --- a/app/channel_category_test.go +++ b/app/channel_category_test.go @@ -136,3 +136,524 @@ func TestGetSidebarCategories(t *testing.T) { assert.Equal(t, "app.channel.sidebar_categories.app_error", appErr.Id) }) } + +func TestUpdateSidebarCategories(t *testing.T) { + t.Run("should mute and unmute all channels in a category when it is muted or unmuted", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + categories, err := th.App.GetSidebarCategories(th.BasicUser.Id, th.BasicTeam.Id) + require.Nil(t, err) + + channelsCategory := categories.Categories[1] + + // Create some channels to be part of the channels category + channel1 := th.CreateChannel(th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel1) + + channel2 := th.CreateChannel(th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel2) + + // Mute the category + updated, err := th.App.UpdateSidebarCategories(th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: channelsCategory.Id, + Muted: true, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + }) + require.Nil(t, err) + assert.True(t, updated[0].Muted) + + // Confirm that the channels are now muted + member1, err := th.App.GetChannelMember(channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member1.IsChannelMuted()) + member2, err := th.App.GetChannelMember(channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member2.IsChannelMuted()) + + // Unmute the category + updated, err = th.App.UpdateSidebarCategories(th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: channelsCategory.Id, + Muted: false, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + }) + require.Nil(t, err) + assert.False(t, updated[0].Muted) + + // Confirm that the channels are now unmuted + member1, err = th.App.GetChannelMember(channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member1.IsChannelMuted()) + member2, err = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member2.IsChannelMuted()) + }) + + t.Run("should mute and unmute channels moved from an unmuted category to a muted one and back", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // Create some channels + channel1 := th.CreateChannel(th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel1) + + channel2 := th.CreateChannel(th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel2) + + // And some categories + mutedCategory, err := th.App.CreateSidebarCategory(th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "muted", + Muted: true, + }, + }) + require.Nil(t, err) + require.True(t, mutedCategory.Muted) + + unmutedCategory, err := th.App.CreateSidebarCategory(th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "unmuted", + Muted: false, + }, + Channels: []string{channel1.Id, channel2.Id}, + }) + require.Nil(t, err) + require.False(t, unmutedCategory.Muted) + + // Move the channels + _, err = th.App.UpdateSidebarCategories(th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: mutedCategory.Id, + DisplayName: mutedCategory.DisplayName, + Muted: mutedCategory.Muted, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: unmutedCategory.Id, + DisplayName: unmutedCategory.DisplayName, + Muted: unmutedCategory.Muted, + }, + Channels: []string{}, + }, + }) + require.Nil(t, err) + + // Confirm that the channels are now muted + member1, err := th.App.GetChannelMember(channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member1.IsChannelMuted()) + member2, err := th.App.GetChannelMember(channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member2.IsChannelMuted()) + + // Move the channels back + _, err = th.App.UpdateSidebarCategories(th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: mutedCategory.Id, + DisplayName: mutedCategory.DisplayName, + Muted: mutedCategory.Muted, + }, + Channels: []string{}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: unmutedCategory.Id, + DisplayName: unmutedCategory.DisplayName, + Muted: unmutedCategory.Muted, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + }) + require.Nil(t, err) + + // Confirm that the channels are now unmuted + member1, err = th.App.GetChannelMember(channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member1.IsChannelMuted()) + member2, err = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member2.IsChannelMuted()) + }) + + t.Run("should not mute or unmute channels moved between muted categories", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // Create some channels + channel1 := th.CreateChannel(th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel1) + + channel2 := th.CreateChannel(th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel2) + + // And some categories + category1, err := th.App.CreateSidebarCategory(th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "category1", + Muted: true, + }, + }) + require.Nil(t, err) + require.True(t, category1.Muted) + + category2, err := th.App.CreateSidebarCategory(th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "category2", + Muted: true, + }, + Channels: []string{channel1.Id, channel2.Id}, + }) + require.Nil(t, err) + require.True(t, category2.Muted) + + // Move the unmuted channels + _, err = th.App.UpdateSidebarCategories(th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: category1.Id, + DisplayName: category1.DisplayName, + Muted: category1.Muted, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: category2.Id, + DisplayName: category2.DisplayName, + Muted: category2.Muted, + }, + Channels: []string{}, + }, + }) + require.Nil(t, err) + + // Confirm that the channels are still unmuted + member1, err := th.App.GetChannelMember(channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member1.IsChannelMuted()) + member2, err := th.App.GetChannelMember(channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member2.IsChannelMuted()) + + // Mute the channels manually + _, err = th.App.ToggleMuteChannel(channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + _, err = th.App.ToggleMuteChannel(channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + + // Move the muted channels back + _, err = th.App.UpdateSidebarCategories(th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: category1.Id, + DisplayName: category1.DisplayName, + Muted: category1.Muted, + }, + Channels: []string{}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: category2.Id, + DisplayName: category2.DisplayName, + Muted: category2.Muted, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + }) + require.Nil(t, err) + + // Confirm that the channels are still muted + member1, err = th.App.GetChannelMember(channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member1.IsChannelMuted()) + member2, err = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member2.IsChannelMuted()) + }) + + t.Run("should not mute or unmute channels moved between unmuted categories", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + // Create some channels + channel1 := th.CreateChannel(th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel1) + + channel2 := th.CreateChannel(th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel2) + + // And some categories + category1, err := th.App.CreateSidebarCategory(th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "category1", + Muted: false, + }, + }) + require.Nil(t, err) + require.False(t, category1.Muted) + + category2, err := th.App.CreateSidebarCategory(th.BasicUser.Id, th.BasicTeam.Id, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "category2", + Muted: false, + }, + Channels: []string{channel1.Id, channel2.Id}, + }) + require.Nil(t, err) + require.False(t, category2.Muted) + + // Move the unmuted channels + _, err = th.App.UpdateSidebarCategories(th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: category1.Id, + DisplayName: category1.DisplayName, + Muted: category1.Muted, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: category2.Id, + DisplayName: category2.DisplayName, + Muted: category2.Muted, + }, + Channels: []string{}, + }, + }) + require.Nil(t, err) + + // Confirm that the channels are still unmuted + member1, err := th.App.GetChannelMember(channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member1.IsChannelMuted()) + member2, err := th.App.GetChannelMember(channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.False(t, member2.IsChannelMuted()) + + // Mute the channels manually + _, err = th.App.ToggleMuteChannel(channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + _, err = th.App.ToggleMuteChannel(channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + + // Move the muted channels back + _, err = th.App.UpdateSidebarCategories(th.BasicUser.Id, th.BasicTeam.Id, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: category1.Id, + DisplayName: category1.DisplayName, + Muted: category1.Muted, + }, + Channels: []string{}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: category2.Id, + DisplayName: category2.DisplayName, + Muted: category2.Muted, + }, + Channels: []string{channel1.Id, channel2.Id}, + }, + }) + require.Nil(t, err) + + // Confirm that the channels are still muted + member1, err = th.App.GetChannelMember(channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member1.IsChannelMuted()) + member2, err = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + assert.True(t, member2.IsChannelMuted()) + }) +} + +func TestDiffChannelsBetweenCategories(t *testing.T) { + t.Run("should return nothing when the categories contain identical channels", func(t *testing.T) { + originalCategories := []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: "category1", + DisplayName: "Category One", + }, + Channels: []string{"channel1", "channel2", "channel3"}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: "category2", + DisplayName: "Category Two", + }, + Channels: []string{"channel4", "channel5"}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: "category3", + DisplayName: "Category Three", + }, + Channels: []string{}, + }, + } + + updatedCategories := []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: "category1", + DisplayName: "Category Won", + }, + Channels: []string{"channel1", "channel2", "channel3"}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: "category2", + DisplayName: "Category Too", + }, + Channels: []string{"channel4", "channel5"}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: "category3", + DisplayName: "Category 🌲", + }, + Channels: []string{}, + }, + } + + channelsDiff := diffChannelsBetweenCategories(updatedCategories, originalCategories) + assert.Equal(t, map[string]*categoryChannelDiff{}, channelsDiff) + }) + + t.Run("should return nothing when the categories contain identical channels", func(t *testing.T) { + originalCategories := []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: "category1", + DisplayName: "Category One", + }, + Channels: []string{"channel1", "channel2", "channel3"}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: "category2", + DisplayName: "Category Two", + }, + Channels: []string{"channel4", "channel5"}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: "category3", + DisplayName: "Category Three", + }, + Channels: []string{}, + }, + } + + updatedCategories := []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: "category1", + DisplayName: "Category Won", + }, + Channels: []string{}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: "category2", + DisplayName: "Category Too", + }, + Channels: []string{"channel5", "channel2"}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: "category3", + DisplayName: "Category 🌲", + }, + Channels: []string{"channel4", "channel1", "channel3"}, + }, + } + + channelsDiff := diffChannelsBetweenCategories(updatedCategories, originalCategories) + assert.Equal( + t, + map[string]*categoryChannelDiff{ + "channel1": { + fromCategoryId: "category1", + toCategoryId: "category3", + }, + "channel2": { + fromCategoryId: "category1", + toCategoryId: "category2", + }, + "channel3": { + fromCategoryId: "category1", + toCategoryId: "category3", + }, + "channel4": { + fromCategoryId: "category2", + toCategoryId: "category3", + }, + }, + channelsDiff, + ) + }) + + t.Run("should not return channels that are moved in our out of the categories implicitly", func(t *testing.T) { + // This case could change to actually return the channels in the future, but we don't need to handle it right now + originalCategories := []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: "category1", + DisplayName: "Category One", + }, + Channels: []string{"channel1", "channel2"}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: "category2", + DisplayName: "Category Two", + }, + Channels: []string{"channel3"}, + }, + } + + updatedCategories := []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: "category1", + DisplayName: "Category Won", + }, + Channels: []string{"channel1", "channel3"}, + }, + { + SidebarCategory: model.SidebarCategory{ + Id: "category2", + DisplayName: "Category Too", + }, + Channels: []string{"channel4"}, + }, + } + + channelsDiff := diffChannelsBetweenCategories(updatedCategories, originalCategories) + assert.Equal( + t, + map[string]*categoryChannelDiff{ + "channel3": { + fromCategoryId: "category2", + toCategoryId: "category1", + }, + }, + channelsDiff, + ) + }) +} diff --git a/app/channel_test.go b/app/channel_test.go index 2850323a42..beea013dd7 100644 --- a/app/channel_test.go +++ b/app/channel_test.go @@ -624,6 +624,57 @@ func TestAppUpdateChannelScheme(t *testing.T) { } } +func TestSetChannelsMuted(t *testing.T) { + t.Run("should mute and unmute the given channels", func(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + channel1 := th.BasicChannel + + channel2 := th.CreateChannel(th.BasicTeam) + th.AddUserToChannel(th.BasicUser, channel2) + + // Ensure that both channels start unmuted + member1, err := th.App.GetChannelMember(channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + require.False(t, member1.IsChannelMuted()) + + member2, err := th.App.GetChannelMember(channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + require.False(t, member2.IsChannelMuted()) + + // Mute both channels + updated, err := th.App.setChannelsMuted([]string{channel1.Id, channel2.Id}, th.BasicUser.Id, true) + require.Nil(t, err) + assert.True(t, updated[0].IsChannelMuted()) + assert.True(t, updated[1].IsChannelMuted()) + + // Verify that the channels are muted in the database + member1, err = th.App.GetChannelMember(channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + require.True(t, member1.IsChannelMuted()) + + member2, err = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + require.True(t, member2.IsChannelMuted()) + + // Unm both channels + updated, err = th.App.setChannelsMuted([]string{channel1.Id, channel2.Id}, th.BasicUser.Id, false) + require.Nil(t, err) + assert.False(t, updated[0].IsChannelMuted()) + assert.False(t, updated[1].IsChannelMuted()) + + // Verify that the channels are muted in the database + member1, err = th.App.GetChannelMember(channel1.Id, th.BasicUser.Id) + require.Nil(t, err) + require.False(t, member1.IsChannelMuted()) + + member2, err = th.App.GetChannelMember(channel2.Id, th.BasicUser.Id) + require.Nil(t, err) + require.False(t, member2.IsChannelMuted()) + }) +} + func TestFillInChannelProps(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() diff --git a/app/opentracing/opentracing_layer.go b/app/opentracing/opentracing_layer.go index b74433b718..cfa37f3d93 100644 --- a/app/opentracing/opentracing_layer.go +++ b/app/opentracing/opentracing_layer.go @@ -14212,7 +14212,7 @@ func (a *OpenTracingAppLayer) TestSiteURL(siteURL string) *model.AppError { return resultVar0 } -func (a *OpenTracingAppLayer) ToggleMuteChannel(channelId string, userId string) *model.ChannelMember { +func (a *OpenTracingAppLayer) ToggleMuteChannel(channelId string, userId string) (*model.ChannelMember, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ToggleMuteChannel") @@ -14224,9 +14224,14 @@ func (a *OpenTracingAppLayer) ToggleMuteChannel(channelId string, userId string) }() defer span.Finish() - resultVar0 := a.app.ToggleMuteChannel(channelId, userId) + resultVar0, resultVar1 := a.app.ToggleMuteChannel(channelId, userId) - return resultVar0 + if resultVar1 != nil { + span.LogFields(spanlog.Error(resultVar1)) + ext.Error.Set(span, true) + } + + return resultVar0, resultVar1 } func (a *OpenTracingAppLayer) TotalWebsocketConnections() int { diff --git a/app/slashcommands/command_mute.go b/app/slashcommands/command_mute.go index a78e1f968b..5443615c82 100644 --- a/app/slashcommands/command_mute.go +++ b/app/slashcommands/command_mute.go @@ -61,36 +61,23 @@ func (me *MuteProvider) DoCommand(a *app.App, args *model.CommandArgs, message s } } - channelMember := a.ToggleMuteChannel(channel.Id, args.UserId) - if channelMember == nil { + channelMember, err := a.ToggleMuteChannel(channel.Id, args.UserId) + if err != nil { return &model.CommandResponse{Text: args.T("api.command_mute.not_member.error", map[string]interface{}{"Channel": channelName}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } - // Invalidate cache to allow cache lookups while sending notifications - a.Srv().Store.Channel().InvalidateCacheForChannelMembersNotifyProps(channel.Id) - // Direct and Group messages won't have a nice channel title, omit it if channel.Type == model.CHANNEL_DIRECT || channel.Type == model.CHANNEL_GROUP { if channelMember.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CHANNEL_NOTIFY_MENTION { - publishChannelMemberEvt(a, channelMember, args.UserId) return &model.CommandResponse{Text: args.T("api.command_mute.success_mute_direct_msg"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } else { - publishChannelMemberEvt(a, channelMember, args.UserId) return &model.CommandResponse{Text: args.T("api.command_mute.success_unmute_direct_msg"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } } if channelMember.NotifyProps[model.MARK_UNREAD_NOTIFY_PROP] == model.CHANNEL_NOTIFY_MENTION { - publishChannelMemberEvt(a, channelMember, args.UserId) return &model.CommandResponse{Text: args.T("api.command_mute.success_mute", map[string]interface{}{"Channel": channel.DisplayName}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } else { - publishChannelMemberEvt(a, channelMember, args.UserId) return &model.CommandResponse{Text: args.T("api.command_mute.success_unmute", map[string]interface{}{"Channel": channel.DisplayName}), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL} } } - -func publishChannelMemberEvt(a *app.App, channelMember *model.ChannelMember, userId string) { - evt := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_MEMBER_UPDATED, "", "", userId, nil) - evt.Add("channelMember", channelMember.ToJson()) - a.Publish(evt) -} diff --git a/model/channel_member.go b/model/channel_member.go index e38bfffe48..d7a76e2d4c 100644 --- a/model/channel_member.go +++ b/model/channel_member.go @@ -164,6 +164,18 @@ func (o *ChannelMember) GetRoles() []string { return strings.Fields(o.Roles) } +func (o *ChannelMember) SetChannelMuted(muted bool) { + if o.IsChannelMuted() { + o.NotifyProps[MARK_UNREAD_NOTIFY_PROP] = CHANNEL_MARK_UNREAD_ALL + } else { + o.NotifyProps[MARK_UNREAD_NOTIFY_PROP] = CHANNEL_MARK_UNREAD_MENTION + } +} + +func (o *ChannelMember) IsChannelMuted() bool { + return o.NotifyProps[MARK_UNREAD_NOTIFY_PROP] == CHANNEL_MARK_UNREAD_MENTION +} + func IsChannelNotifyLevelValid(notifyLevel string) bool { return notifyLevel == CHANNEL_NOTIFY_DEFAULT || notifyLevel == CHANNEL_NOTIFY_ALL || diff --git a/model/channel_sidebar.go b/model/channel_sidebar.go index d05c6c9db4..033432c902 100644 --- a/model/channel_sidebar.go +++ b/model/channel_sidebar.go @@ -46,6 +46,7 @@ type SidebarCategory struct { Sorting SidebarCategorySorting `json:"sorting"` Type SidebarCategoryType `json:"type"` DisplayName string `json:"display_name"` + Muted bool `json:"muted"` } // SidebarCategoryWithChannels combines data from SidebarCategory table with the Channel IDs that belong to that category diff --git a/store/opentracinglayer/opentracinglayer.go b/store/opentracinglayer/opentracinglayer.go index 3d61617bac..1463d68b32 100644 --- a/store/opentracinglayer/opentracinglayer.go +++ b/store/opentracinglayer/opentracinglayer.go @@ -1286,6 +1286,24 @@ func (s *OpenTracingLayerChannelStore) GetMembers(channelId string, offset int, return result, err } +func (s *OpenTracingLayerChannelStore) GetMembersByChannelIds(channelIds []string, userId string) (*model.ChannelMembers, error) { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersByChannelIds") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + result, err := s.ChannelStore.GetMembersByChannelIds(channelIds, userId) + if err != nil { + span.LogFields(spanlog.Error(err)) + ext.Error.Set(span, true) + } + + return result, err +} + func (s *OpenTracingLayerChannelStore) GetMembersByIds(channelId string, userIds []string) (*model.ChannelMembers, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetMembersByIds") @@ -2164,7 +2182,7 @@ func (s *OpenTracingLayerChannelStore) UpdateMultipleMembers(members []*model.Ch return result, err } -func (s *OpenTracingLayerChannelStore) UpdateSidebarCategories(userId string, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, error) { +func (s *OpenTracingLayerChannelStore) UpdateSidebarCategories(userId string, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.UpdateSidebarCategories") s.Root.Store.SetContext(newCtx) @@ -2173,13 +2191,13 @@ func (s *OpenTracingLayerChannelStore) UpdateSidebarCategories(userId string, te }() defer span.Finish() - result, err := s.ChannelStore.UpdateSidebarCategories(userId, teamId, categories) + result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userId, teamId, categories) if err != nil { span.LogFields(spanlog.Error(err)) ext.Error.Set(span, true) } - return result, err + return result, resultVar1, err } func (s *OpenTracingLayerChannelStore) UpdateSidebarCategoryOrder(userId string, teamId string, categoryOrder []string) error { diff --git a/store/retrylayer/retrylayer.go b/store/retrylayer/retrylayer.go index 57a3d5a59a..013ad0d3e2 100644 --- a/store/retrylayer/retrylayer.go +++ b/store/retrylayer/retrylayer.go @@ -1390,6 +1390,26 @@ func (s *RetryLayerChannelStore) GetMembers(channelId string, offset int, limit } +func (s *RetryLayerChannelStore) GetMembersByChannelIds(channelIds []string, userId string) (*model.ChannelMembers, error) { + + tries := 0 + for { + result, err := s.ChannelStore.GetMembersByChannelIds(channelIds, userId) + if err == nil { + return result, nil + } + if !isRepeatableError(err) { + return result, err + } + tries++ + if tries >= 3 { + err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") + return result, err + } + } + +} + func (s *RetryLayerChannelStore) GetMembersByIds(channelId string, userIds []string) (*model.ChannelMembers, error) { tries := 0 @@ -2298,21 +2318,21 @@ func (s *RetryLayerChannelStore) UpdateMultipleMembers(members []*model.ChannelM } -func (s *RetryLayerChannelStore) UpdateSidebarCategories(userId string, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, error) { +func (s *RetryLayerChannelStore) UpdateSidebarCategories(userId string, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) { tries := 0 for { - result, err := s.ChannelStore.UpdateSidebarCategories(userId, teamId, categories) + result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userId, teamId, categories) if err == nil { - return result, nil + return result, resultVar1, nil } if !isRepeatableError(err) { - return result, err + return result, resultVar1, err } tries++ if tries >= 3 { err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures") - return result, err + return result, resultVar1, err } } diff --git a/store/sqlstore/channel_store.go b/store/sqlstore/channel_store.go index 06fd2436e7..b676a7c579 100644 --- a/store/sqlstore/channel_store.go +++ b/store/sqlstore/channel_store.go @@ -2955,27 +2955,30 @@ func (s SqlChannelStore) SearchGroupChannels(userId, term string) (*model.Channe func (s SqlChannelStore) GetMembersByIds(channelId string, userIds []string) (*model.ChannelMembers, error) { var dbMembers channelMemberWithSchemeRolesList - props := make(map[string]interface{}) - idQuery := "" - - for index, userId := range userIds { - if len(idQuery) > 0 { - idQuery += ", " - } - - props["userId"+strconv.Itoa(index)] = userId - idQuery += ":userId" + strconv.Itoa(index) - } + keys, props := MapStringsToQueryParams(userIds, "User") props["ChannelId"] = channelId - if _, err := s.GetReplica().Select(&dbMembers, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.ChannelId = :ChannelId AND ChannelMembers.UserId IN ("+idQuery+")", props); err != nil { + if _, err := s.GetReplica().Select(&dbMembers, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.ChannelId = :ChannelId AND ChannelMembers.UserId IN "+keys, props); err != nil { return nil, errors.Wrapf(err, "failed to find ChannelMembers with channelId=%s and userId in %v", channelId, userIds) } return dbMembers.ToModel(), nil } +func (s SqlChannelStore) GetMembersByChannelIds(channelIds []string, userId string) (*model.ChannelMembers, error) { + var dbMembers channelMemberWithSchemeRolesList + + keys, props := MapStringsToQueryParams(channelIds, "Channel") + props["UserId"] = userId + + if _, err := s.GetReplica().Select(&dbMembers, CHANNEL_MEMBERS_WITH_SCHEME_SELECT_QUERY+"WHERE ChannelMembers.UserId = :UserId AND ChannelMembers.ChannelId IN "+keys, props); err != nil { + return nil, errors.Wrapf(err, "failed to find ChannelMembers with userId=%s and channelId in %v", userId, channelIds) + } + + return dbMembers.ToModel(), nil +} + func (s SqlChannelStore) GetChannelsByScheme(schemeId string, offset int, limit int) (model.ChannelList, error) { var channels model.ChannelList _, err := s.GetReplica().Select(&channels, "SELECT * FROM Channels WHERE SchemeId = :SchemeId ORDER BY DisplayName LIMIT :Limit OFFSET :Offset", map[string]interface{}{"SchemeId": schemeId, "Offset": offset, "Limit": limit}) diff --git a/store/sqlstore/channel_store_categories.go b/store/sqlstore/channel_store_categories.go index bdf6cb9112..82cb4168df 100644 --- a/store/sqlstore/channel_store_categories.go +++ b/store/sqlstore/channel_store_categories.go @@ -266,6 +266,7 @@ func (s SqlChannelStore) CreateSidebarCategory(userId, teamId string, newCategor Sorting: model.SidebarCategorySortDefault, SortOrder: int64(model.MinimalSidebarSortDistance * len(newOrder)), // first we place it at the end of the list Type: model.SidebarCategoryCustom, + Muted: newCategory.Muted, } if err = transaction.Insert(category); err != nil { return nil, errors.Wrap(err, "failed to save SidebarCategory") @@ -605,18 +606,19 @@ func (s SqlChannelStore) UpdateSidebarCategoryOrder(userId, teamId string, categ return nil } -func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, error) { +func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) { transaction, err := s.GetMaster().Begin() if err != nil { - return nil, errors.Wrap(err, "begin_transaction") + return nil, nil, errors.Wrap(err, "begin_transaction") } defer finalizeTransaction(transaction) updatedCategories := []*model.SidebarCategoryWithChannels{} + originalCategories := []*model.SidebarCategoryWithChannels{} for _, category := range categories { originalCategory, err2 := s.GetSidebarCategory(category.Id) if err2 != nil { - return nil, errors.Wrap(err2, "failed to find SidebarCategories") + return nil, nil, errors.Wrap(err2, "failed to find SidebarCategories") } // Copy category to avoid modifying an argument @@ -629,24 +631,28 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori updatedCategory.TeamId = originalCategory.TeamId updatedCategory.SortOrder = originalCategory.SortOrder updatedCategory.Type = originalCategory.Type + updatedCategory.Muted = originalCategory.Muted if updatedCategory.Type != model.SidebarCategoryCustom { updatedCategory.DisplayName = originalCategory.DisplayName } - if category.Type != model.SidebarCategoryDirectMessages { + if updatedCategory.Type != model.SidebarCategoryDirectMessages { updatedCategory.Channels = make([]string, len(category.Channels)) copy(updatedCategory.Channels, category.Channels) + + updatedCategory.Muted = category.Muted } updateQuery, updateParams, _ := s.getQueryBuilder(). Update("SidebarCategories"). Set("DisplayName", updatedCategory.DisplayName). Set("Sorting", updatedCategory.Sorting). + Set("Muted", updatedCategory.Muted). Where(sq.Eq{"Id": updatedCategory.Id}).ToSql() if _, err = transaction.Exec(updateQuery, updateParams...); err != nil { - return nil, errors.Wrap(err, "failed to update SidebarCategories") + return nil, nil, errors.Wrap(err, "failed to update SidebarCategories") } // if we are updating DM category, it's order can't channel order cannot be changed. @@ -667,11 +673,11 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori ).ToSql() if err2 != nil { - return nil, errors.Wrap(err2, "update_sidebar_catetories_tosql") + return nil, nil, errors.Wrap(err2, "update_sidebar_catetories_tosql") } if _, err = transaction.Exec(query, args...); err != nil { - return nil, errors.Wrap(err, "failed to delete SidebarChannels") + return nil, nil, errors.Wrap(err, "failed to delete SidebarChannels") } var channels []interface{} @@ -687,7 +693,7 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori } if err = transaction.Insert(channels...); err != nil { - return nil, errors.Wrap(err, "failed to save SidebarChannels") + return nil, nil, errors.Wrap(err, "failed to save SidebarChannels") } } @@ -703,7 +709,7 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori ).ToSql() if _, err = transaction.Exec(sql, args...); err != nil { - return nil, errors.Wrap(err, "failed to delete Preferences") + return nil, nil, errors.Wrap(err, "failed to delete Preferences") } // And then add the new ones @@ -716,7 +722,7 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori Category: model.PREFERENCE_CATEGORY_FAVORITE_CHANNEL, Value: "true", }); err != nil { - return nil, errors.Wrap(err, "failed to save Preference") + return nil, nil, errors.Wrap(err, "failed to save Preference") } } } else { @@ -729,32 +735,33 @@ func (s SqlChannelStore) UpdateSidebarCategories(userId, teamId string, categori }, ).ToSql() if nErr != nil { - return nil, errors.Wrap(nErr, "update_sidebar_categories_tosql") + return nil, nil, errors.Wrap(nErr, "update_sidebar_categories_tosql") } if _, nErr = transaction.Exec(query, args...); nErr != nil { - return nil, errors.Wrap(nErr, "failed to delete Preferences") + return nil, nil, errors.Wrap(nErr, "failed to delete Preferences") } } updatedCategories = append(updatedCategories, updatedCategory) + originalCategories = append(originalCategories, originalCategory) } // Ensure Channels are populated for Channels/Direct Messages category if they change for i, updatedCategory := range updatedCategories { populated, nErr := s.completePopulatingCategoryChannelsT(transaction, updatedCategory) if nErr != nil { - return nil, nErr + return nil, nil, nErr } updatedCategories[i] = populated } if err = transaction.Commit(); err != nil { - return nil, errors.Wrap(err, "commit_transaction") + return nil, nil, errors.Wrap(err, "commit_transaction") } - return updatedCategories, nil + return updatedCategories, originalCategories, nil } // UpdateSidebarChannelsByPreferences is called when the Preference table is being updated to keep SidebarCategories in sync diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 6c6a273e35..bc16a2095c 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -20,6 +20,7 @@ import ( const ( CURRENT_SCHEMA_VERSION = VERSION_5_29_0 + VERSION_5_30_0 = "5.30.0" VERSION_5_29_0 = "5.29.0" VERSION_5_28_1 = "5.28.1" VERSION_5_28_0 = "5.28.0" @@ -866,15 +867,6 @@ func upgradeDatabaseToVersion5281(sqlStore SqlStore) { } } -func upgradeDatabaseToVersion530(sqlStore SqlStore) { - // if shouldPerformUpgrade(sqlStore, VERSION_5_29_0, VERSION_5_30_0) { - - sqlStore.CreateColumnIfNotExistsNoDefault("FileInfo", "Content", "longtext", "text") - - // saveSchemaVersion(sqlStore, VERSION_5_30_0) - // } -} - func precheckMigrationToVersion528(sqlStore SqlStore) error { teamsQuery, _, err := sqlStore.getQueryBuilder().Select(`COALESCE(SUM(CASE WHEN CHAR_LENGTH(SchemeId) > 26 THEN 1 @@ -948,3 +940,14 @@ func upgradeDatabaseToVersion529(sqlStore SqlStore) { saveSchemaVersion(sqlStore, VERSION_5_29_0) } } + +func upgradeDatabaseToVersion530(sqlStore SqlStore) { + // if shouldPerformUpgrade(sqlStore, VERSION_5_29_0, VERSION_5_30_0) { + + sqlStore.CreateColumnIfNotExistsNoDefault("FileInfo", "Content", "longtext", "text") + + sqlStore.CreateColumnIfNotExists("SidebarCategories", "Muted", "tinyint(1)", "boolean", "0") + + // saveSchemaVersion(sqlStore, VERSION_5_30_0) + // } +} diff --git a/store/store.go b/store/store.go index 82a6b9d7bf..bc6b283b2b 100644 --- a/store/store.go +++ b/store/store.go @@ -207,6 +207,7 @@ type ChannelStore interface { SearchMore(userId string, teamId string, term string) (*model.ChannelList, error) SearchGroupChannels(userId, term string) (*model.ChannelList, error) GetMembersByIds(channelId string, userIds []string) (*model.ChannelMembers, error) + GetMembersByChannelIds(channelIds []string, userId string) (*model.ChannelMembers, error) AnalyticsDeletedTypeCount(teamId string, channelType string) (int64, error) GetChannelUnread(channelId, userId string) (*model.ChannelUnread, error) ClearCaches() @@ -221,7 +222,7 @@ type ChannelStore interface { GetSidebarCategoryOrder(userId, teamId string) ([]string, error) CreateSidebarCategory(userId, teamId string, newCategory *model.SidebarCategoryWithChannels) (*model.SidebarCategoryWithChannels, error) UpdateSidebarCategoryOrder(userId, teamId string, categoryOrder []string) error - UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, error) + UpdateSidebarCategories(userId, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) UpdateSidebarChannelsByPreferences(preferences *model.Preferences) error DeleteSidebarChannelsByPreferences(preferences *model.Preferences) error DeleteSidebarCategory(categoryId string) error diff --git a/store/storetest/channel_store.go b/store/storetest/channel_store.go index 473b84728c..c38fbbec55 100644 --- a/store/storetest/channel_store.go +++ b/store/storetest/channel_store.go @@ -84,6 +84,7 @@ func TestChannelStore(t *testing.T, ss store.Store, s SqlSupplier) { t.Run("SearchForUserInTeam", func(t *testing.T) { testChannelStoreSearchForUserInTeam(t, ss) }) t.Run("SearchAllChannels", func(t *testing.T) { testChannelStoreSearchAllChannels(t, ss) }) t.Run("GetMembersByIds", func(t *testing.T) { testChannelStoreGetMembersByIds(t, ss) }) + t.Run("GetMembersByChannelIds", func(t *testing.T) { testChannelStoreGetMembersByChannelIds(t, ss) }) t.Run("SearchGroupChannels", func(t *testing.T) { testChannelStoreSearchGroupChannels(t, ss) }) t.Run("AnalyticsDeletedTypeCount", func(t *testing.T) { testChannelStoreAnalyticsDeletedTypeCount(t, ss) }) t.Run("GetPinnedPosts", func(t *testing.T) { testChannelStoreGetPinnedPosts(t, ss) }) @@ -5546,6 +5547,64 @@ func testChannelStoreGetMembersByIds(t *testing.T, ss store.Store) { require.NotNil(t, nErr, "empty user ids - should have failed") } +func testChannelStoreGetMembersByChannelIds(t *testing.T, ss store.Store) { + userId := model.NewId() + + // Create a couple channels and add the user to them + channel1, err := ss.Channel().Save(&model.Channel{ + TeamId: model.NewId(), + DisplayName: model.NewId(), + Name: model.NewId(), + Type: model.CHANNEL_OPEN, + }, -1) + require.Nil(t, err) + + channel2, err := ss.Channel().Save(&model.Channel{ + TeamId: model.NewId(), + DisplayName: model.NewId(), + Name: model.NewId(), + Type: model.CHANNEL_OPEN, + }, -1) + require.Nil(t, err) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel1.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + _, err = ss.Channel().SaveMember(&model.ChannelMember{ + ChannelId: channel2.Id, + UserId: userId, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + t.Run("should return the user's members for the given channels", func(t *testing.T) { + result, nErr := ss.Channel().GetMembersByChannelIds([]string{channel1.Id, channel2.Id}, userId) + require.Nil(t, nErr) + assert.Len(t, *result, 2) + + assert.Equal(t, userId, (*result)[0].UserId) + assert.True(t, (*result)[0].ChannelId == channel1.Id || (*result)[1].ChannelId == channel1.Id) + assert.Equal(t, userId, (*result)[1].UserId) + assert.True(t, (*result)[0].ChannelId == channel2.Id || (*result)[1].ChannelId == channel2.Id) + }) + + t.Run("should not error or return anything for invalid channel IDs", func(t *testing.T) { + result, nErr := ss.Channel().GetMembersByChannelIds([]string{model.NewId(), model.NewId()}, userId) + require.Nil(t, nErr) + assert.Len(t, *result, 0) + }) + + t.Run("should not error or return anything for invalid user IDs", func(t *testing.T) { + result, nErr := ss.Channel().GetMembersByChannelIds([]string{channel1.Id, channel2.Id}, model.NewId()) + require.Nil(t, nErr) + assert.Len(t, *result, 0) + }) +} + func testChannelStoreSearchGroupChannels(t *testing.T, ss store.Store) { // Users u1 := &model.User{} diff --git a/store/storetest/channel_store_categories.go b/store/storetest/channel_store_categories.go index 305a51d4f0..01493b1f70 100644 --- a/store/storetest/channel_store_categories.go +++ b/store/storetest/channel_store_categories.go @@ -478,7 +478,7 @@ func testCreateSidebarCategory(t *testing.T, ss store.Store) { // Assign them to categories favoritesCategory.Channels = []string{channel1.Id} channelsCategory.Channels = []string{channel2.Id} - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ favoritesCategory, channelsCategory, }) @@ -683,7 +683,7 @@ func testGetSidebarCategory(t *testing.T, ss store.Store, s SqlSupplier) { require.Nil(t, nErr) // And assign one to another category - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory.SidebarCategory, Channels: []string{channel2.Id}, @@ -884,7 +884,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { dmsCategory := initialCategories.Categories[2] // And then update one of them - updated, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + updated, _, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ channelsCategory, }) require.Nil(t, err) @@ -917,7 +917,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { dmsCategory := initialCategories.Categories[2] // And then update them - updatedCategories, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ favoritesCategory, channelsCategory, dmsCategory, @@ -980,7 +980,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { }, } - updatedCategories, err := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) + updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) assert.Nil(t, err) assert.NotEqual(t, "Favorites", categoriesToUpdate[0].DisplayName) @@ -1022,7 +1022,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { require.Nil(t, nErr) // Assign it to favorites - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory.SidebarCategory, Channels: []string{channel.Id}, @@ -1039,7 +1039,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { channelsCategory := categories.Categories[1] require.Equal(t, model.SidebarCategoryChannels, channelsCategory.Type) - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: channelsCategory.SidebarCategory, Channels: []string{channel.Id}, @@ -1087,7 +1087,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { assert.Nil(t, nErr) // Assign it to favorites - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory.SidebarCategory, Channels: []string{dmChannel.Id}, @@ -1104,7 +1104,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { dmsCategory := categories.Categories[2] require.Equal(t, model.SidebarCategoryDirectMessages, dmsCategory.Type) - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: dmsCategory.SidebarCategory, Channels: []string{dmChannel.Id}, @@ -1162,7 +1162,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { assert.Nil(t, nErr) // Assign it to favorites on the first team. The favorites preference gets set for all teams. - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory.SidebarCategory, Channels: []string{dmChannel.Id}, @@ -1176,7 +1176,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { assert.Equal(t, "true", res.Value) // Assign it to favorites on the second team. The favorites preference is already set. - updated, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + updated, _, err := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory2.SidebarCategory, Channels: []string{dmChannel.Id}, @@ -1191,7 +1191,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { assert.Equal(t, "true", res.Value) // Remove it from favorites on the first team. This clears the favorites preference for all teams. - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory.SidebarCategory, Channels: []string{}, @@ -1204,7 +1204,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { assert.Nil(t, res) // Remove it from favorites on the second team. The favorites preference was already deleted. - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId2, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId2, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory2.SidebarCategory, Channels: []string{}, @@ -1268,7 +1268,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { require.Nil(t, nErr) // Have user1 favorite it - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory.SidebarCategory, Channels: []string{channel.Id}, @@ -1290,7 +1290,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { assert.Nil(t, res) // And user2 favorite it - _, err = ss.Channel().UpdateSidebarCategories(userId2, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId2, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: favoritesCategory2.SidebarCategory, Channels: []string{channel.Id}, @@ -1313,7 +1313,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { assert.Equal(t, "true", res.Value) // And then user1 unfavorite it - _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: channelsCategory.SidebarCategory, Channels: []string{channel.Id}, @@ -1335,7 +1335,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { assert.Equal(t, "true", res.Value) // And finally user2 favorite it - _, err = ss.Channel().UpdateSidebarCategories(userId2, teamId, []*model.SidebarCategoryWithChannels{ + _, _, err = ss.Channel().UpdateSidebarCategories(userId2, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: channelsCategory2.SidebarCategory, Channels: []string{channel.Id}, @@ -1416,7 +1416,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { }, } - updatedCategories, nErr := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) + updatedCategories, _, nErr := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) assert.Nil(t, nErr) // The channels should still exist in the category because they would otherwise be orphaned @@ -1470,7 +1470,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { }, } - updatedCategories, err := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) + updatedCategories, _, err := ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) assert.Nil(t, err) assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id) assert.Equal(t, []string{}, updatedCategories[0].Channels) @@ -1497,7 +1497,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { }, } - updatedCategories, err = ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) + updatedCategories, _, err = ss.Channel().UpdateSidebarCategories(userId, teamId, categoriesToUpdate) assert.Nil(t, err) assert.Equal(t, dmsCategory.Id, updatedCategories[0].Id) assert.Equal(t, []string{dmChannel.Id}, updatedCategories[0].Channels) @@ -1545,7 +1545,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { require.Nil(t, nErr) // Move the channel one way - updatedCategories, nErr := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + updatedCategories, _, nErr := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: channelsCategory.SidebarCategory, Channels: []string{}, @@ -1561,7 +1561,7 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { assert.Equal(t, []string{channel.Id}, updatedCategories[1].Channels) // And then the other - updatedCategories, nErr = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + updatedCategories, _, nErr = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ { SidebarCategory: channelsCategory.SidebarCategory, Channels: []string{channel.Id}, @@ -1575,6 +1575,77 @@ func testUpdateSidebarCategories(t *testing.T, ss store.Store, s SqlSupplier) { assert.Equal(t, []string{channel.Id}, updatedCategories[0].Channels) assert.Equal(t, []string{}, updatedCategories[1].Channels) }) + + t.Run("should correctly return the original categories that were modified", func(t *testing.T) { + userId := model.NewId() + teamId := model.NewId() + + // Join a channel + channel, nErr := ss.Channel().Save(&model.Channel{ + Name: "channel", + Type: model.CHANNEL_OPEN, + TeamId: teamId, + }, 10) + require.Nil(t, nErr) + _, err := ss.Channel().SaveMember(&model.ChannelMember{ + UserId: userId, + ChannelId: channel.Id, + NotifyProps: model.GetDefaultChannelNotifyProps(), + }) + require.Nil(t, err) + + // And then create the initial categories so that Channels includes the channel + nErr = ss.Channel().CreateInitialSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + initialCategories, nErr := ss.Channel().GetSidebarCategories(userId, teamId) + require.Nil(t, nErr) + + channelsCategory := initialCategories.Categories[1] + require.Equal(t, []string{channel.Id}, channelsCategory.Channels) + + customCategory, nErr := ss.Channel().CreateSidebarCategory(userId, teamId, &model.SidebarCategoryWithChannels{ + SidebarCategory: model.SidebarCategory{ + DisplayName: "originalName", + }, + }) + require.Nil(t, nErr) + + // Rename the custom category + updatedCategories, originalCategories, nErr := ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: model.SidebarCategory{ + Id: customCategory.Id, + DisplayName: "updatedName", + }, + }, + }) + require.Nil(t, nErr) + require.Equal(t, len(updatedCategories), len(originalCategories)) + assert.Equal(t, "originalName", originalCategories[0].DisplayName) + assert.Equal(t, "updatedName", updatedCategories[0].DisplayName) + + // Move a channel + updatedCategories, originalCategories, nErr = ss.Channel().UpdateSidebarCategories(userId, teamId, []*model.SidebarCategoryWithChannels{ + { + SidebarCategory: channelsCategory.SidebarCategory, + Channels: []string{}, + }, + { + SidebarCategory: customCategory.SidebarCategory, + Channels: []string{channel.Id}, + }, + }) + require.Nil(t, nErr) + require.Equal(t, len(updatedCategories), len(originalCategories)) + require.Equal(t, updatedCategories[0].Id, originalCategories[0].Id) + require.Equal(t, updatedCategories[1].Id, originalCategories[1].Id) + + assert.Equal(t, []string{channel.Id}, originalCategories[0].Channels) + assert.Equal(t, []string{}, updatedCategories[0].Channels) + assert.Equal(t, []string{}, originalCategories[1].Channels) + assert.Equal(t, []string{channel.Id}, updatedCategories[1].Channels) + }) } func testDeleteSidebarCategory(t *testing.T, ss store.Store, s SqlSupplier) { diff --git a/store/storetest/mocks/ChannelStore.go b/store/storetest/mocks/ChannelStore.go index 249c14a3eb..2304e0082e 100644 --- a/store/storetest/mocks/ChannelStore.go +++ b/store/storetest/mocks/ChannelStore.go @@ -934,6 +934,29 @@ func (_m *ChannelStore) GetMembers(channelId string, offset int, limit int) (*mo return r0, r1 } +// GetMembersByChannelIds provides a mock function with given fields: channelIds, userId +func (_m *ChannelStore) GetMembersByChannelIds(channelIds []string, userId string) (*model.ChannelMembers, error) { + ret := _m.Called(channelIds, userId) + + var r0 *model.ChannelMembers + if rf, ok := ret.Get(0).(func([]string, string) *model.ChannelMembers); ok { + r0 = rf(channelIds, userId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.ChannelMembers) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func([]string, string) error); ok { + r1 = rf(channelIds, userId) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetMembersByIds provides a mock function with given fields: channelId, userIds func (_m *ChannelStore) GetMembersByIds(channelId string, userIds []string) (*model.ChannelMembers, error) { ret := _m.Called(channelId, userIds) @@ -1859,7 +1882,7 @@ func (_m *ChannelStore) UpdateMultipleMembers(members []*model.ChannelMember) ([ } // UpdateSidebarCategories provides a mock function with given fields: userId, teamId, categories -func (_m *ChannelStore) UpdateSidebarCategories(userId string, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, error) { +func (_m *ChannelStore) UpdateSidebarCategories(userId string, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) { ret := _m.Called(userId, teamId, categories) var r0 []*model.SidebarCategoryWithChannels @@ -1871,14 +1894,23 @@ func (_m *ChannelStore) UpdateSidebarCategories(userId string, teamId string, ca } } - var r1 error - if rf, ok := ret.Get(1).(func(string, string, []*model.SidebarCategoryWithChannels) error); ok { + var r1 []*model.SidebarCategoryWithChannels + if rf, ok := ret.Get(1).(func(string, string, []*model.SidebarCategoryWithChannels) []*model.SidebarCategoryWithChannels); ok { r1 = rf(userId, teamId, categories) } else { - r1 = ret.Error(1) + if ret.Get(1) != nil { + r1 = ret.Get(1).([]*model.SidebarCategoryWithChannels) + } } - return r0, r1 + var r2 error + if rf, ok := ret.Get(2).(func(string, string, []*model.SidebarCategoryWithChannels) error); ok { + r2 = rf(userId, teamId, categories) + } else { + r2 = ret.Error(2) + } + + return r0, r1, r2 } // UpdateSidebarCategoryOrder provides a mock function with given fields: userId, teamId, categoryOrder diff --git a/store/timerlayer/timerlayer.go b/store/timerlayer/timerlayer.go index 8d0af3f610..d416a4004b 100644 --- a/store/timerlayer/timerlayer.go +++ b/store/timerlayer/timerlayer.go @@ -1191,6 +1191,22 @@ func (s *TimerLayerChannelStore) GetMembers(channelId string, offset int, limit return result, err } +func (s *TimerLayerChannelStore) GetMembersByChannelIds(channelIds []string, userId string) (*model.ChannelMembers, error) { + start := timemodule.Now() + + result, err := s.ChannelStore.GetMembersByChannelIds(channelIds, userId) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if err == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetMembersByChannelIds", success, elapsed) + } + return result, err +} + func (s *TimerLayerChannelStore) GetMembersByIds(channelId string, userIds []string) (*model.ChannelMembers, error) { start := timemodule.Now() @@ -2000,10 +2016,10 @@ func (s *TimerLayerChannelStore) UpdateMultipleMembers(members []*model.ChannelM return result, err } -func (s *TimerLayerChannelStore) UpdateSidebarCategories(userId string, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, error) { +func (s *TimerLayerChannelStore) UpdateSidebarCategories(userId string, teamId string, categories []*model.SidebarCategoryWithChannels) ([]*model.SidebarCategoryWithChannels, []*model.SidebarCategoryWithChannels, error) { start := timemodule.Now() - result, err := s.ChannelStore.UpdateSidebarCategories(userId, teamId, categories) + result, resultVar1, err := s.ChannelStore.UpdateSidebarCategories(userId, teamId, categories) elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) if s.Root.Metrics != nil { @@ -2013,7 +2029,7 @@ func (s *TimerLayerChannelStore) UpdateSidebarCategories(userId string, teamId s } s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.UpdateSidebarCategories", success, elapsed) } - return result, err + return result, resultVar1, err } func (s *TimerLayerChannelStore) UpdateSidebarCategoryOrder(userId string, teamId string, categoryOrder []string) error {