mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
MM42267: Add member count in the browse channel modal (#23800)
* add base for calling the endpoint
* add endpoint and handler
* update store and layers
* call the endpoint
* align types
* update app layers
* generate mocks
* complete handler
* finish store query
* add todos
* add ui for member count
* add selector
* add a todo
* add cache layer
* optimize calls in FE
* handle invalidation of the cache
* fix go style
* fix test
* use existing channel layer count
* fix import error
* delete unnecessary code
* write tests for channel cache layer
* fix testname
* fix mocks
* fix cache layer test
* fix a test
* really fix the test
* write more tests for server
* address PR comments
* remove comment
* rename more_channels to browse_channels
* fix style
* update snapshot
* add translations
* Revert "add translations"
This reverts commit 56476a5dab
.
* add only related translations
* address PR review points
* add test
* fix test
---------
Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
parent
4803889158
commit
628273d98d
@ -98,7 +98,7 @@ describe('Leave an archived channel', () => {
|
|||||||
cy.get('#showMoreChannels').click();
|
cy.get('#showMoreChannels').click();
|
||||||
|
|
||||||
// # More channels modal opens
|
// # More channels modal opens
|
||||||
cy.get('#moreChannelsModal').should('be.visible').within(() => {
|
cy.get('#browseChannelsModal').should('be.visible').within(() => {
|
||||||
// # Click on dropdown
|
// # Click on dropdown
|
||||||
cy.findByText(channelType.public).should('be.visible').click();
|
cy.findByText(channelType.public).should('be.visible').click();
|
||||||
|
|
||||||
@ -146,7 +146,7 @@ describe('Leave an archived channel', () => {
|
|||||||
cy.get('#showMoreChannels').click();
|
cy.get('#showMoreChannels').click();
|
||||||
|
|
||||||
// # More channels modal opens
|
// # More channels modal opens
|
||||||
cy.get('#moreChannelsModal').should('be.visible').within(() => {
|
cy.get('#browseChannelsModal').should('be.visible').within(() => {
|
||||||
// # Public channel list opens by default
|
// # Public channel list opens by default
|
||||||
cy.findByText(channelType.public).should('be.visible').click();
|
cy.findByText(channelType.public).should('be.visible').click();
|
||||||
|
|
||||||
@ -199,7 +199,7 @@ describe('Leave an archived channel', () => {
|
|||||||
cy.get('#showMoreChannels').click();
|
cy.get('#showMoreChannels').click();
|
||||||
|
|
||||||
// # More channels modal opens
|
// # More channels modal opens
|
||||||
cy.get('#moreChannelsModal').should('be.visible').within(() => {
|
cy.get('#browseChannelsModal').should('be.visible').within(() => {
|
||||||
// # Public channels are shown by default
|
// # Public channels are shown by default
|
||||||
cy.findByText(channelType.public).should('be.visible').click();
|
cy.findByText(channelType.public).should('be.visible').click();
|
||||||
|
|
||||||
@ -253,7 +253,7 @@ describe('Leave an archived channel', () => {
|
|||||||
cy.get('#showMoreChannels').click();
|
cy.get('#showMoreChannels').click();
|
||||||
|
|
||||||
// # More channels modal opens
|
// # More channels modal opens
|
||||||
cy.get('#moreChannelsModal').should('be.visible').within(() => {
|
cy.get('#browseChannelsModal').should('be.visible').within(() => {
|
||||||
// # Show public channels is visible by default
|
// # Show public channels is visible by default
|
||||||
cy.findByText(channelType.public).should('be.visible').click();
|
cy.findByText(channelType.public).should('be.visible').click();
|
||||||
|
|
||||||
@ -288,7 +288,7 @@ describe('Leave an archived channel', () => {
|
|||||||
cy.get('#showMoreChannels').click();
|
cy.get('#showMoreChannels').click();
|
||||||
|
|
||||||
// # More channels modal opens and lands on public channels
|
// # More channels modal opens and lands on public channels
|
||||||
cy.get('#moreChannelsModal').should('be.visible').within(() => {
|
cy.get('#browseChannelsModal').should('be.visible').within(() => {
|
||||||
cy.findByText(channelType.public).should('be.visible').click();
|
cy.findByText(channelType.public).should('be.visible').click();
|
||||||
|
|
||||||
// # Go to archived channels
|
// # Go to archived channels
|
||||||
|
@ -48,9 +48,9 @@ describe('Channels', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('MM-19337 Verify UI of More channels modal with archived selection', () => {
|
it('MM-19337 Verify UI of Browse channels modal with archived selection', () => {
|
||||||
verifyMoreChannelsModalWithArchivedSelection(false, testUser, testTeam);
|
verifyBrowseChannelsModalWithArchivedSelection(false, testUser, testTeam);
|
||||||
verifyMoreChannelsModalWithArchivedSelection(true, testUser, testTeam);
|
verifyBrowseChannelsModalWithArchivedSelection(true, testUser, testTeam);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('MM-19337 Enable users to view archived channels', () => {
|
it('MM-19337 Enable users to view archived channels', () => {
|
||||||
@ -68,7 +68,7 @@ describe('Channels', () => {
|
|||||||
// # Go to LHS and click 'Browse channels'
|
// # Go to LHS and click 'Browse channels'
|
||||||
cy.uiBrowseOrCreateChannel('Browse channels').click();
|
cy.uiBrowseOrCreateChannel('Browse channels').click();
|
||||||
|
|
||||||
cy.get('#moreChannelsModal').should('be.visible').within(() => {
|
cy.get('#browseChannelsModal').should('be.visible').within(() => {
|
||||||
// * Dropdown should be visible, defaulting to "Public Channels"
|
// * Dropdown should be visible, defaulting to "Public Channels"
|
||||||
cy.get('#channelsMoreDropdown').should('be.visible').and('contain', channelType.public).wait(TIMEOUTS.HALF_SEC);
|
cy.get('#channelsMoreDropdown').should('be.visible').and('contain', channelType.public).wait(TIMEOUTS.HALF_SEC);
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ describe('Channels', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// # Verify that the modal is not closed
|
// # Verify that the modal is not closed
|
||||||
cy.get('#moreChannelsModal').should('exist');
|
cy.get('#browseChannelsModal').should('exist');
|
||||||
cy.url().should('include', `/${testTeam.name}/channels/${testChannel.name}`);
|
cy.url().should('include', `/${testTeam.name}/channels/${testChannel.name}`);
|
||||||
|
|
||||||
// # Login as channel admin and go directly to the channel
|
// # Login as channel admin and go directly to the channel
|
||||||
@ -111,7 +111,7 @@ describe('Channels', () => {
|
|||||||
// # Go to LHS and click 'Browse channels'
|
// # Go to LHS and click 'Browse channels'
|
||||||
cy.uiBrowseOrCreateChannel('Browse channels').click();
|
cy.uiBrowseOrCreateChannel('Browse channels').click();
|
||||||
|
|
||||||
cy.get('#moreChannelsModal').should('be.visible').within(() => {
|
cy.get('#browseChannelsModal').should('be.visible').within(() => {
|
||||||
// # CLick dropdown to open selection
|
// # CLick dropdown to open selection
|
||||||
cy.get('#channelsMoreDropdown').should('be.visible').click().within((el) => {
|
cy.get('#channelsMoreDropdown').should('be.visible').click().within((el) => {
|
||||||
// # Click on archived channels item
|
// # Click on archived channels item
|
||||||
@ -145,6 +145,38 @@ describe('Channels', () => {
|
|||||||
cy.get('#sidebar-left').should('not.contain', testChannel.display_name);
|
cy.get('#sidebar-left').should('not.contain', testChannel.display_name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('MM-19337 Increase channel member count when user joins a channel', () => {
|
||||||
|
// # Login as new user and go to "/"
|
||||||
|
cy.apiLogin(otherUser);
|
||||||
|
cy.visit(`/${testTeam.name}/channels/town-square`);
|
||||||
|
|
||||||
|
let newChannel;
|
||||||
|
cy.apiCreateChannel(testTeam.id, 'channel-to-leave', 'Channel to leave').then(({channel}) => {
|
||||||
|
newChannel = channel;
|
||||||
|
cy.visit(`/${testTeam.name}/channels/${newChannel.name}`);
|
||||||
|
|
||||||
|
// # Leave the channel
|
||||||
|
cy.uiLeaveChannel();
|
||||||
|
|
||||||
|
// * Verify that we've switched to Town Square
|
||||||
|
cy.url().should('include', '/channels/town-square');
|
||||||
|
});
|
||||||
|
|
||||||
|
// # Go to LHS and click 'Browse channels'
|
||||||
|
cy.uiBrowseOrCreateChannel('Browse channels').click();
|
||||||
|
|
||||||
|
cy.get('#browseChannelsModal').should('be.visible').within(() => {
|
||||||
|
// * Verify that channel has zero members
|
||||||
|
cy.findByTestId(`channelMemberCount-${newChannel.name}`).should('be.visible').and('contain', 0);
|
||||||
|
|
||||||
|
// # Click the channel row to join the channel
|
||||||
|
cy.findByTestId(`ChannelRow-${newChannel.name}`).scrollIntoView().should('be.visible').click();
|
||||||
|
|
||||||
|
// * Verify that channel has one member
|
||||||
|
cy.findByTestId(`channelMemberCount-${newChannel.name}`).should('be.visible').and('contain', 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('MM-T1702 Search works when changing public/archived options in the dropdown', () => {
|
it('MM-T1702 Search works when changing public/archived options in the dropdown', () => {
|
||||||
cy.apiAdminLogin();
|
cy.apiAdminLogin();
|
||||||
cy.apiUpdateConfig({
|
cy.apiUpdateConfig({
|
||||||
@ -210,7 +242,7 @@ describe('Channels', () => {
|
|||||||
cy.findByText(newChannel.display_name).should('be.visible');
|
cy.findByText(newChannel.display_name).should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get('#moreChannelsModal').should('be.visible').within(() => {
|
cy.get('#browseChannelsModal').should('be.visible').within(() => {
|
||||||
// * Users should be able to switch to "Archived Channels" list
|
// * Users should be able to switch to "Archived Channels" list
|
||||||
cy.get('#channelsMoreDropdown').should('be.visible').and('contain', channelType.public).click().within((el) => {
|
cy.get('#channelsMoreDropdown').should('be.visible').and('contain', channelType.public).click().within((el) => {
|
||||||
// # Click on archived channels item
|
// # Click on archived channels item
|
||||||
@ -229,7 +261,7 @@ describe('Channels', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function verifyMoreChannelsModalWithArchivedSelection(isEnabled, testUser, testTeam) {
|
function verifyBrowseChannelsModalWithArchivedSelection(isEnabled, testUser, testTeam) {
|
||||||
// # Login as sysadmin and Update config to enable/disable viewing of archived channels
|
// # Login as sysadmin and Update config to enable/disable viewing of archived channels
|
||||||
cy.apiAdminLogin();
|
cy.apiAdminLogin();
|
||||||
cy.apiUpdateConfig({
|
cy.apiUpdateConfig({
|
||||||
@ -238,22 +270,22 @@ function verifyMoreChannelsModalWithArchivedSelection(isEnabled, testUser, testT
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// * Verify more channels modal
|
// * Verify browse channels modal
|
||||||
cy.visit(`/${testTeam.name}/channels/town-square`);
|
cy.visit(`/${testTeam.name}/channels/town-square`);
|
||||||
verifyMoreChannelsModal(isEnabled);
|
verifyBrowseChannelsModal(isEnabled);
|
||||||
|
|
||||||
// # Login as regular user and verify more channels modal
|
// # Login as regular user and verify browse channels modal
|
||||||
cy.apiLogin(testUser);
|
cy.apiLogin(testUser);
|
||||||
cy.visit(`/${testTeam.name}/channels/town-square`);
|
cy.visit(`/${testTeam.name}/channels/town-square`);
|
||||||
verifyMoreChannelsModal(isEnabled);
|
verifyBrowseChannelsModal(isEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
function verifyMoreChannelsModal(isEnabled) {
|
function verifyBrowseChannelsModal(isEnabled) {
|
||||||
// # Go to LHS and click 'Browse channels'
|
// # Go to LHS and click 'Browse channels'
|
||||||
cy.uiBrowseOrCreateChannel('Browse channels').click();
|
cy.uiBrowseOrCreateChannel('Browse channels').click();
|
||||||
|
|
||||||
// * Verify that the more channels modal is open and with or without option to view archived channels
|
// * Verify that the browse channels modal is open and with or without option to view archived channels
|
||||||
cy.get('#moreChannelsModal').should('be.visible').within(() => {
|
cy.get('#browseChannelsModal').should('be.visible').within(() => {
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', channelType.public);
|
cy.get('#channelsMoreDropdown').should('be.visible').and('have.text', channelType.public);
|
||||||
} else {
|
} else {
|
@ -53,8 +53,12 @@ describe('more public channels', () => {
|
|||||||
|
|
||||||
// * Assert that the moreChannelsModel is visible
|
// * Assert that the moreChannelsModel is visible
|
||||||
cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').within(() => {
|
cy.findByRole('dialog', {name: 'Browse Channels'}).should('be.visible').within(() => {
|
||||||
// # Click hide joined checkbox
|
// # Click hide joined checkbox if not already checked
|
||||||
cy.findByText('Hide Joined').should('be.visible').click();
|
cy.findByText('Hide Joined').should('be.visible').then(($checkbox) => {
|
||||||
|
if (!$checkbox.prop('checked')) {
|
||||||
|
cy.wrap($checkbox).click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// * Assert that the moreChannelsList is visible and the number of channels is 31
|
// * Assert that the moreChannelsList is visible and the number of channels is 31
|
||||||
cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 31);
|
cy.get('#moreChannelsList').should('be.visible').children().should('have.length', 31);
|
||||||
|
@ -68,13 +68,13 @@ describe('Channel sidebar', () => {
|
|||||||
cy.get('.AddChannelDropdown .MenuItem:contains(Browse channels) button').should('be.visible').click();
|
cy.get('.AddChannelDropdown .MenuItem:contains(Browse channels) button').should('be.visible').click();
|
||||||
|
|
||||||
// * Verify that the more channels modal is visible
|
// * Verify that the more channels modal is visible
|
||||||
cy.get('#moreChannelsModal').should('be.visible');
|
cy.get('#browseChannelsModal').should('be.visible');
|
||||||
|
|
||||||
// Click the Off-Topic channel
|
// Click the Off-Topic channel
|
||||||
cy.findByText('Off-Topic').should('be.visible').click();
|
cy.findByText('Off-Topic').should('be.visible').click();
|
||||||
|
|
||||||
// Verify that new channel is in the sidebar and is active
|
// Verify that new channel is in the sidebar and is active
|
||||||
cy.get('#moreChannelsModal').should('exist');
|
cy.get('#browseChannelsModal').should('exist');
|
||||||
cy.url().should('include', `/${teamName}/channels/off-topic`);
|
cy.url().should('include', `/${teamName}/channels/off-topic`);
|
||||||
cy.get('#channelHeaderTitle').should('contain', 'Off-Topic');
|
cy.get('#channelHeaderTitle').should('contain', 'Off-Topic');
|
||||||
cy.get('.SidebarChannel.active:contains(Off-Topic)').should('be.visible');
|
cy.get('.SidebarChannel.active:contains(Off-Topic)').should('be.visible');
|
||||||
|
@ -25,6 +25,7 @@ func (api *API) InitChannel() {
|
|||||||
api.BaseRoutes.Channels.Handle("/group", api.APISessionRequired(createGroupChannel)).Methods("POST")
|
api.BaseRoutes.Channels.Handle("/group", api.APISessionRequired(createGroupChannel)).Methods("POST")
|
||||||
api.BaseRoutes.Channels.Handle("/members/{user_id:[A-Za-z0-9]+}/view", api.APISessionRequired(viewChannel)).Methods("POST")
|
api.BaseRoutes.Channels.Handle("/members/{user_id:[A-Za-z0-9]+}/view", api.APISessionRequired(viewChannel)).Methods("POST")
|
||||||
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/scheme", api.APISessionRequired(updateChannelScheme)).Methods("PUT")
|
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/scheme", api.APISessionRequired(updateChannelScheme)).Methods("PUT")
|
||||||
|
api.BaseRoutes.Channels.Handle("/stats/member_count", api.APISessionRequired(getChannelsMemberCount)).Methods("POST")
|
||||||
|
|
||||||
api.BaseRoutes.ChannelsForTeam.Handle("", api.APISessionRequired(getPublicChannelsForTeam)).Methods("GET")
|
api.BaseRoutes.ChannelsForTeam.Handle("", api.APISessionRequired(getPublicChannelsForTeam)).Methods("GET")
|
||||||
api.BaseRoutes.ChannelsForTeam.Handle("/deleted", api.APISessionRequired(getDeletedChannelsForTeam)).Methods("GET")
|
api.BaseRoutes.ChannelsForTeam.Handle("/deleted", api.APISessionRequired(getDeletedChannelsForTeam)).Methods("GET")
|
||||||
@ -681,6 +682,29 @@ func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getChannelsMemberCount(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if c.Err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channelIDs := model.ArrayFromJSON(r.Body)
|
||||||
|
if !c.App.SessionHasPermissionToChannels(c.AppContext, *c.AppContext.Session(), channelIDs, model.PermissionReadChannel) {
|
||||||
|
c.SetPermissionError(model.PermissionReadChannel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
channelsMemberCount, err := c.App.GetChannelsMemberCount(c.AppContext, channelIDs)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.NewEncoder(w).Encode(channelsMemberCount); err != nil {
|
||||||
|
c.Logger.Warn("Error while writing response", mlog.Err(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getPinnedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
func getPinnedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
c.RequireChannelId()
|
c.RequireChannelId()
|
||||||
if c.Err != nil {
|
if c.Err != nil {
|
||||||
|
@ -4469,6 +4469,74 @@ func TestGetChannelMemberCountsByGroup(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetChannelsMemberCount(t *testing.T) {
|
||||||
|
// Setup
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
client := th.Client
|
||||||
|
|
||||||
|
channel1 := th.CreatePublicChannel()
|
||||||
|
channel2 := th.CreatePublicChannel()
|
||||||
|
|
||||||
|
user1 := th.CreateUser()
|
||||||
|
user2 := th.CreateUser()
|
||||||
|
user3 := th.CreateUser()
|
||||||
|
|
||||||
|
th.LinkUserToTeam(user1, th.BasicTeam)
|
||||||
|
th.LinkUserToTeam(user2, th.BasicTeam)
|
||||||
|
th.LinkUserToTeam(user3, th.BasicTeam)
|
||||||
|
|
||||||
|
th.AddUserToChannel(user1, channel1)
|
||||||
|
th.AddUserToChannel(user2, channel1)
|
||||||
|
th.AddUserToChannel(user3, channel1)
|
||||||
|
th.AddUserToChannel(user2, channel2)
|
||||||
|
|
||||||
|
t.Run("Should return correct member count", func(t *testing.T) {
|
||||||
|
// Create a request with channel IDs
|
||||||
|
channelIDs := []string{channel1.Id, channel2.Id}
|
||||||
|
channelsMemberCount, _, err := client.GetChannelsMemberCount(context.Background(), channelIDs)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify the member counts
|
||||||
|
require.Contains(t, channelsMemberCount, channel1.Id)
|
||||||
|
require.Contains(t, channelsMemberCount, channel2.Id)
|
||||||
|
require.Equal(t, int64(4), channelsMemberCount[channel1.Id])
|
||||||
|
require.Equal(t, int64(2), channelsMemberCount[channel2.Id])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should return empty object when empty array is passed", func(t *testing.T) {
|
||||||
|
channelsMemberCount, _, err := client.GetChannelsMemberCount(context.Background(), []string{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 0, len(channelsMemberCount))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should fail due to permissions", func(t *testing.T) {
|
||||||
|
_, resp, err := client.GetChannelsMemberCount(context.Background(), []string{"junk"})
|
||||||
|
require.Error(t, err)
|
||||||
|
CheckForbiddenStatus(t, resp)
|
||||||
|
CheckErrorID(t, err, "api.context.permissions.app_error")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should fail due to expired session when logged out", func(t *testing.T) {
|
||||||
|
client.Logout(context.Background())
|
||||||
|
channelIDs := []string{channel1.Id, channel2.Id}
|
||||||
|
_, resp, err := client.GetChannelsMemberCount(context.Background(), channelIDs)
|
||||||
|
require.Error(t, err)
|
||||||
|
CheckUnauthorizedStatus(t, resp)
|
||||||
|
CheckErrorID(t, err, "api.context.session_expired.app_error")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should fail due to expired session when logged out", func(t *testing.T) {
|
||||||
|
th.LoginBasic2()
|
||||||
|
channelIDs := []string{channel1.Id, channel2.Id}
|
||||||
|
_, resp, err := client.GetChannelsMemberCount(context.Background(), channelIDs)
|
||||||
|
require.Error(t, err)
|
||||||
|
CheckForbiddenStatus(t, resp)
|
||||||
|
CheckErrorID(t, err, "api.context.permissions.app_error")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestMoveChannel(t *testing.T) {
|
func TestMoveChannel(t *testing.T) {
|
||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
@ -625,6 +625,7 @@ type AppIface interface {
|
|||||||
GetChannelsForTeamForUser(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError)
|
GetChannelsForTeamForUser(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts) (model.ChannelList, *model.AppError)
|
||||||
GetChannelsForTeamForUserWithCursor(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, *model.AppError)
|
GetChannelsForTeamForUserWithCursor(c request.CTX, teamID string, userID string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, *model.AppError)
|
||||||
GetChannelsForUser(c request.CTX, userID string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, *model.AppError)
|
GetChannelsForUser(c request.CTX, userID string, includeDeleted bool, lastDeleteAt, pageSize int, fromChannelID string) (model.ChannelList, *model.AppError)
|
||||||
|
GetChannelsMemberCount(c request.CTX, channelIDs []string) (map[string]int64, *model.AppError)
|
||||||
GetChannelsUserNotIn(c request.CTX, teamID string, userID string, offset int, limit int) (model.ChannelList, *model.AppError)
|
GetChannelsUserNotIn(c request.CTX, teamID string, userID string, offset int, limit int) (model.ChannelList, *model.AppError)
|
||||||
GetCloudSession(token string) (*model.Session, *model.AppError)
|
GetCloudSession(token string) (*model.Session, *model.AppError)
|
||||||
GetClusterId() string
|
GetClusterId() string
|
||||||
|
@ -1834,6 +1834,20 @@ func (a *App) GetChannels(c request.CTX, channelIDs []string) ([]*model.Channel,
|
|||||||
return channels, nil
|
return channels, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) GetChannelsMemberCount(c request.CTX, channelIDs []string) (map[string]int64, *model.AppError) {
|
||||||
|
channelsCount, err := a.Srv().Store().Channel().GetChannelsMemberCount(channelIDs)
|
||||||
|
if err != nil {
|
||||||
|
var nfErr *store.ErrNotFound
|
||||||
|
switch {
|
||||||
|
case errors.As(err, &nfErr):
|
||||||
|
return nil, model.NewAppError("GetChannelsMemberCount", "app.channel.get_channels_member_count.existing.app_error", nil, "", http.StatusNotFound).Wrap(err)
|
||||||
|
default:
|
||||||
|
return nil, model.NewAppError("GetChannelsMemberCount", "app.channel.get_channels_member_count.find.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return channelsCount, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) GetChannelByName(c request.CTX, channelName, teamID string, includeDeleted bool) (*model.Channel, *model.AppError) {
|
func (a *App) GetChannelByName(c request.CTX, channelName, teamID string, includeDeleted bool) (*model.Channel, *model.AppError) {
|
||||||
var channel *model.Channel
|
var channel *model.Channel
|
||||||
var err error
|
var err error
|
||||||
|
@ -2195,6 +2195,24 @@ func TestGetMemberCountsByGroup(t *testing.T) {
|
|||||||
require.ElementsMatch(t, cmc, resp)
|
require.ElementsMatch(t, cmc, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetChannelsMemberCount(t *testing.T) {
|
||||||
|
th := SetupWithStoreMock(t)
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
mockStore := th.App.Srv().Store().(*mocks.Store)
|
||||||
|
mockChannelStore := mocks.ChannelStore{}
|
||||||
|
channelsMemberCount := map[string]int64{
|
||||||
|
"channel1": int64(10),
|
||||||
|
"channel2": int64(20),
|
||||||
|
}
|
||||||
|
mockChannelStore.On("GetChannelsMemberCount", []string{"channel1", "channel2"}).Return(channelsMemberCount, nil)
|
||||||
|
mockStore.On("Channel").Return(&mockChannelStore)
|
||||||
|
mockStore.On("GetDBSchemaVersion").Return(1, nil)
|
||||||
|
resp, err := th.App.GetChannelsMemberCount(th.Context, []string{"channel1", "channel2"})
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, channelsMemberCount, resp)
|
||||||
|
}
|
||||||
|
|
||||||
func TestViewChannelCollapsedThreadsTurnedOff(t *testing.T) {
|
func TestViewChannelCollapsedThreadsTurnedOff(t *testing.T) {
|
||||||
th := Setup(t).InitBasic()
|
th := Setup(t).InitBasic()
|
||||||
defer th.TearDown()
|
defer th.TearDown()
|
||||||
|
@ -5612,6 +5612,28 @@ func (a *OpenTracingAppLayer) GetChannelsForUser(c request.CTX, userID string, i
|
|||||||
return resultVar0, resultVar1
|
return resultVar0, resultVar1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *OpenTracingAppLayer) GetChannelsMemberCount(c request.CTX, channelIDs []string) (map[string]int64, *model.AppError) {
|
||||||
|
origCtx := a.ctx
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsMemberCount")
|
||||||
|
|
||||||
|
a.ctx = newCtx
|
||||||
|
a.app.Srv().Store().SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
a.app.Srv().Store().SetContext(origCtx)
|
||||||
|
a.ctx = origCtx
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
resultVar0, resultVar1 := a.app.GetChannelsMemberCount(c, channelIDs)
|
||||||
|
|
||||||
|
if resultVar1 != nil {
|
||||||
|
span.LogFields(spanlog.Error(resultVar1))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultVar0, resultVar1
|
||||||
|
}
|
||||||
|
|
||||||
func (a *OpenTracingAppLayer) GetChannelsUserNotIn(c request.CTX, teamID string, userID string, offset int, limit int) (model.ChannelList, *model.AppError) {
|
func (a *OpenTracingAppLayer) GetChannelsUserNotIn(c request.CTX, teamID string, userID string, offset int, limit int) (model.ChannelList, *model.AppError) {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsUserNotIn")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetChannelsUserNotIn")
|
||||||
|
@ -225,6 +225,35 @@ func (s LocalCacheChannelStore) SaveMultipleMembers(members []*model.ChannelMemb
|
|||||||
return members, nil
|
return members, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s LocalCacheChannelStore) GetChannelsMemberCount(channelIDs []string) (_ map[string]int64, err error) {
|
||||||
|
counts := make(map[string]int64)
|
||||||
|
remainingChannels := make([]string, 0)
|
||||||
|
|
||||||
|
for _, channelID := range channelIDs {
|
||||||
|
var cacheItem int64
|
||||||
|
err := s.rootStore.doStandardReadCache(s.rootStore.channelMemberCountsCache, channelID, &cacheItem)
|
||||||
|
if err == nil {
|
||||||
|
counts[channelID] = cacheItem
|
||||||
|
} else {
|
||||||
|
remainingChannels = append(remainingChannels, channelID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(remainingChannels) > 0 {
|
||||||
|
remainingChannels, err := s.ChannelStore.GetChannelsMemberCount(remainingChannels)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, count := range remainingChannels {
|
||||||
|
s.rootStore.doStandardAddToCache(s.rootStore.channelMemberCountsCache, id, count)
|
||||||
|
counts[id] = count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return counts, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s LocalCacheChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
|
func (s LocalCacheChannelStore) UpdateMember(member *model.ChannelMember) (*model.ChannelMember, error) {
|
||||||
member, err := s.ChannelStore.UpdateMember(member)
|
member, err := s.ChannelStore.UpdateMember(member)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -104,6 +104,43 @@ func TestChannelStoreChannelMemberCountsCache(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestChannelStoreChannelsMemberCountCache(t *testing.T) {
|
||||||
|
channelsCountResult := map[string]int64{
|
||||||
|
"channel1": 10,
|
||||||
|
"channel2": 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("first call not cached, second cached and returning same data", func(t *testing.T) {
|
||||||
|
mockStore := getMockStore()
|
||||||
|
mockCacheProvider := getMockCacheProvider()
|
||||||
|
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
channelsCount, err := cachedStore.Channel().GetChannelsMemberCount([]string{"channel1", "channel2"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, channelsCount, channelsCountResult)
|
||||||
|
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetChannelsMemberCount", 1)
|
||||||
|
channelsCount, err = cachedStore.Channel().GetChannelsMemberCount([]string{"channel1", "channel2"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, channelsCount, channelsCountResult)
|
||||||
|
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetChannelsMemberCount", 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("first call not cached, invalidate cache, second call not cached", func(t *testing.T) {
|
||||||
|
mockStore := getMockStore()
|
||||||
|
mockCacheProvider := getMockCacheProvider()
|
||||||
|
cachedStore, err := NewLocalCacheLayer(mockStore, nil, nil, mockCacheProvider)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cachedStore.Channel().GetChannelsMemberCount([]string{"channel1", "channel2"})
|
||||||
|
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetChannelsMemberCount", 1)
|
||||||
|
cachedStore.Channel().InvalidateMemberCount("channel1")
|
||||||
|
cachedStore.Channel().InvalidateMemberCount("channel2")
|
||||||
|
cachedStore.Channel().GetChannelsMemberCount([]string{"channel1", "channel2"})
|
||||||
|
mockStore.Channel().(*mocks.ChannelStore).AssertNumberOfCalls(t, "GetChannelsMemberCount", 2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestChannelStoreChannelPinnedPostsCountsCache(t *testing.T) {
|
func TestChannelStoreChannelPinnedPostsCountsCache(t *testing.T) {
|
||||||
countResult := int64(10)
|
countResult := int64(10)
|
||||||
|
|
||||||
|
@ -100,6 +100,12 @@ func getMockStore() *mocks.Store {
|
|||||||
mockChannelStore.On("Get", channelId, false).Return(&fakeChannelId, nil)
|
mockChannelStore.On("Get", channelId, false).Return(&fakeChannelId, nil)
|
||||||
mockStore.On("Channel").Return(&mockChannelStore)
|
mockStore.On("Channel").Return(&mockChannelStore)
|
||||||
|
|
||||||
|
mockChannelsMemberCount := map[string]int64{
|
||||||
|
"channel1": 10,
|
||||||
|
"channel2": 20,
|
||||||
|
}
|
||||||
|
mockChannelStore.On("GetChannelsMemberCount", []string{"channel1", "channel2"}).Return(mockChannelsMemberCount, nil)
|
||||||
|
|
||||||
mockPinnedPostsCount := int64(10)
|
mockPinnedPostsCount := int64(10)
|
||||||
mockChannelStore.On("GetPinnedPostCount", "id", true).Return(mockPinnedPostsCount, nil)
|
mockChannelStore.On("GetPinnedPostCount", "id", true).Return(mockPinnedPostsCount, nil)
|
||||||
mockChannelStore.On("GetPinnedPostCount", "id", false).Return(mockPinnedPostsCount, nil)
|
mockChannelStore.On("GetPinnedPostCount", "id", false).Return(mockPinnedPostsCount, nil)
|
||||||
|
@ -1269,6 +1269,24 @@ func (s *OpenTracingLayerChannelStore) GetChannelsByUser(userID string, includeD
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *OpenTracingLayerChannelStore) GetChannelsMemberCount(channelIDs []string) (map[string]int64, error) {
|
||||||
|
origCtx := s.Root.Store.Context()
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsMemberCount")
|
||||||
|
s.Root.Store.SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
s.Root.Store.SetContext(origCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
result, err := s.ChannelStore.GetChannelsMemberCount(channelIDs)
|
||||||
|
if err != nil {
|
||||||
|
span.LogFields(spanlog.Error(err))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *OpenTracingLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
|
func (s *OpenTracingLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
|
||||||
origCtx := s.Root.Store.Context()
|
origCtx := s.Root.Store.Context()
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsWithCursor")
|
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ChannelStore.GetChannelsWithCursor")
|
||||||
|
@ -1406,6 +1406,27 @@ func (s *RetryLayerChannelStore) GetChannelsByUser(userID string, includeDeleted
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *RetryLayerChannelStore) GetChannelsMemberCount(channelIDs []string) (map[string]int64, error) {
|
||||||
|
|
||||||
|
tries := 0
|
||||||
|
for {
|
||||||
|
result, err := s.ChannelStore.GetChannelsMemberCount(channelIDs)
|
||||||
|
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
|
||||||
|
}
|
||||||
|
timepkg.Sleep(100 * timepkg.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func (s *RetryLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
|
func (s *RetryLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
|
||||||
|
|
||||||
tries := 0
|
tries := 0
|
||||||
|
@ -2207,6 +2207,47 @@ func (s SqlChannelStore) GetAllChannelMembersForUser(userId string, allowFromCac
|
|||||||
return ids, nil
|
return ids, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s SqlChannelStore) GetChannelsMemberCount(channelIDs []string) (_ map[string]int64, err error) {
|
||||||
|
query := s.getQueryBuilder().
|
||||||
|
Select("ChannelMembers.ChannelId,COUNT(*) AS Count").
|
||||||
|
From("ChannelMembers").
|
||||||
|
InnerJoin("Users ON ChannelMembers.UserId = Users.Id").
|
||||||
|
Where(sq.And{
|
||||||
|
sq.Eq{"ChannelMembers.ChannelId": channelIDs},
|
||||||
|
sq.Eq{"Users.DeleteAt": 0},
|
||||||
|
}).
|
||||||
|
GroupBy("ChannelMembers.ChannelId")
|
||||||
|
|
||||||
|
queryString, args, err := query.ToSql()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "channels_member_count_tosql")
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := s.GetReplicaX().DB.Query(queryString, args...)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to fetch member counts")
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
memberCounts := make(map[string]int64)
|
||||||
|
for rows.Next() {
|
||||||
|
var channelID string
|
||||||
|
var count int64
|
||||||
|
errScan := rows.Scan(&channelID, &count)
|
||||||
|
if errScan != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to scan row")
|
||||||
|
}
|
||||||
|
memberCounts[channelID] = count
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "error while iterating rows")
|
||||||
|
}
|
||||||
|
|
||||||
|
return memberCounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s SqlChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelId string) {
|
func (s SqlChannelStore) InvalidateCacheForChannelMembersNotifyProps(channelId string) {
|
||||||
allChannelMembersNotifyPropsForChannelCache.Remove(channelId)
|
allChannelMembersNotifyPropsForChannelCache.Remove(channelId)
|
||||||
if s.metrics != nil {
|
if s.metrics != nil {
|
||||||
|
@ -222,6 +222,7 @@ type ChannelStore interface {
|
|||||||
GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error)
|
GetMember(ctx context.Context, channelID string, userID string) (*model.ChannelMember, error)
|
||||||
GetChannelMembersTimezones(channelID string) ([]model.StringMap, error)
|
GetChannelMembersTimezones(channelID string) ([]model.StringMap, error)
|
||||||
GetAllChannelMembersForUser(userID string, allowFromCache bool, includeDeleted bool) (map[string]string, error)
|
GetAllChannelMembersForUser(userID string, allowFromCache bool, includeDeleted bool) (map[string]string, error)
|
||||||
|
GetChannelsMemberCount(channelIDs []string) (map[string]int64, error)
|
||||||
InvalidateAllChannelMembersForUser(userID string)
|
InvalidateAllChannelMembersForUser(userID string)
|
||||||
IsUserInChannelUseCache(userID string, channelID string) bool
|
IsUserInChannelUseCache(userID string, channelID string) bool
|
||||||
GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error)
|
GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error)
|
||||||
|
@ -910,6 +910,32 @@ func (_m *ChannelStore) GetChannelsByUser(userID string, includeDeleted bool, la
|
|||||||
return r0, r1
|
return r0, r1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetChannelsMemberCount provides a mock function with given fields: channelIDs
|
||||||
|
func (_m *ChannelStore) GetChannelsMemberCount(channelIDs []string) (map[string]int64, error) {
|
||||||
|
ret := _m.Called(channelIDs)
|
||||||
|
|
||||||
|
var r0 map[string]int64
|
||||||
|
var r1 error
|
||||||
|
if rf, ok := ret.Get(0).(func([]string) (map[string]int64, error)); ok {
|
||||||
|
return rf(channelIDs)
|
||||||
|
}
|
||||||
|
if rf, ok := ret.Get(0).(func([]string) map[string]int64); ok {
|
||||||
|
r0 = rf(channelIDs)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(map[string]int64)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if rf, ok := ret.Get(1).(func([]string) error); ok {
|
||||||
|
r1 = rf(channelIDs)
|
||||||
|
} else {
|
||||||
|
r1 = ret.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0, r1
|
||||||
|
}
|
||||||
|
|
||||||
// GetChannelsWithCursor provides a mock function with given fields: teamId, userId, opts, afterChannelID
|
// GetChannelsWithCursor provides a mock function with given fields: teamId, userId, opts, afterChannelID
|
||||||
func (_m *ChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
|
func (_m *ChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
|
||||||
ret := _m.Called(teamId, userId, opts, afterChannelID)
|
ret := _m.Called(teamId, userId, opts, afterChannelID)
|
||||||
|
@ -1184,6 +1184,22 @@ func (s *TimerLayerChannelStore) GetChannelsByUser(userID string, includeDeleted
|
|||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *TimerLayerChannelStore) GetChannelsMemberCount(channelIDs []string) (map[string]int64, error) {
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
result, err := s.ChannelStore.GetChannelsMemberCount(channelIDs)
|
||||||
|
|
||||||
|
elapsed := float64(time.Since(start)) / float64(time.Second)
|
||||||
|
if s.Root.Metrics != nil {
|
||||||
|
success := "false"
|
||||||
|
if err == nil {
|
||||||
|
success = "true"
|
||||||
|
}
|
||||||
|
s.Root.Metrics.ObserveStoreMethodDuration("ChannelStore.GetChannelsMemberCount", success, elapsed)
|
||||||
|
}
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *TimerLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
|
func (s *TimerLayerChannelStore) GetChannelsWithCursor(teamId string, userId string, opts *model.ChannelSearchOpts, afterChannelID string) (model.ChannelList, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
@ -4739,6 +4739,14 @@
|
|||||||
"id": "app.channel.get_channels_by_ids.not_found.app_error",
|
"id": "app.channel.get_channels_by_ids.not_found.app_error",
|
||||||
"translation": "No channel found."
|
"translation": "No channel found."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "app.channel.get_channels_member_count.existing.app_error",
|
||||||
|
"translation": "Unable to find member count for given channels."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "app.channel.get_channels_member_count.find.app_error",
|
||||||
|
"translation": "Unable to find member count."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "app.channel.get_deleted.existing.app_error",
|
"id": "app.channel.get_deleted.existing.app_error",
|
||||||
"translation": "Unable to find the existing deleted channel."
|
"translation": "Unable to find the existing deleted channel."
|
||||||
|
@ -3047,6 +3047,21 @@ func (c *Client4) GetChannelStats(ctx context.Context, channelId string, etag st
|
|||||||
return &stats, BuildResponse(r), nil
|
return &stats, BuildResponse(r), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetChannelsMemberCount get channel member count for a given array of channel ids
|
||||||
|
func (c *Client4) GetChannelsMemberCount(ctx context.Context, channelIDs []string) (map[string]int64, *Response, error) {
|
||||||
|
route := c.channelsRoute() + "/stats/member_count"
|
||||||
|
r, err := c.DoAPIPost(ctx, route, ArrayToJSON(channelIDs))
|
||||||
|
if err != nil {
|
||||||
|
return nil, BuildResponse(r), err
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
var counts map[string]int64
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&counts); err != nil {
|
||||||
|
return nil, nil, NewAppError("GetChannelsMemberCount", "api.unmarshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
|
||||||
|
}
|
||||||
|
return counts, BuildResponse(r), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetChannelMembersTimezones gets a list of timezones for a channel.
|
// GetChannelMembersTimezones gets a list of timezones for a channel.
|
||||||
func (c *Client4) GetChannelMembersTimezones(ctx context.Context, channelId string) ([]string, *Response, error) {
|
func (c *Client4) GetChannelMembersTimezones(ctx context.Context, channelId string) ([]string, *Response, error) {
|
||||||
r, err := c.DoAPIGet(ctx, c.channelRoute(channelId)+"/timezones", "")
|
r, err := c.DoAPIGet(ctx, c.channelRoute(channelId)+"/timezones", "")
|
||||||
|
@ -29,6 +29,7 @@ const (
|
|||||||
ClusterEventInvalidateCacheForChannelFileCount ClusterEvent = "inv_channel_file_count"
|
ClusterEventInvalidateCacheForChannelFileCount ClusterEvent = "inv_channel_file_count"
|
||||||
ClusterEventInvalidateCacheForChannelPinnedpostsCounts ClusterEvent = "inv_channel_pinnedposts_counts"
|
ClusterEventInvalidateCacheForChannelPinnedpostsCounts ClusterEvent = "inv_channel_pinnedposts_counts"
|
||||||
ClusterEventInvalidateCacheForChannelMemberCounts ClusterEvent = "inv_channel_member_counts"
|
ClusterEventInvalidateCacheForChannelMemberCounts ClusterEvent = "inv_channel_member_counts"
|
||||||
|
ClusterEventInvalidateCacheForChannelsMemberCount ClusterEvent = "inv_channels_member_count"
|
||||||
ClusterEventInvalidateCacheForLastPosts ClusterEvent = "inv_last_posts"
|
ClusterEventInvalidateCacheForLastPosts ClusterEvent = "inv_last_posts"
|
||||||
ClusterEventInvalidateCacheForLastPostTime ClusterEvent = "inv_last_post_time"
|
ClusterEventInvalidateCacheForLastPostTime ClusterEvent = "inv_last_post_time"
|
||||||
ClusterEventInvalidateCacheForPostsUsage ClusterEvent = "inv_posts_usage"
|
ClusterEventInvalidateCacheForPostsUsage ClusterEvent = "inv_posts_usage"
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`components/MoreChannels should match snapshot and state 1`] = `
|
exports[`components/BrowseChannels should match snapshot and state 1`] = `
|
||||||
<GenericModal
|
<GenericModal
|
||||||
aria-labelledby="moreChannelsModalLabel"
|
aria-labelledby="browseChannelsModalLabel"
|
||||||
aria-modal={true}
|
aria-modal={true}
|
||||||
autoCloseOnCancelButton={true}
|
autoCloseOnCancelButton={true}
|
||||||
autoCloseOnConfirmButton={false}
|
autoCloseOnConfirmButton={false}
|
||||||
@ -32,7 +32,7 @@ exports[`components/MoreChannels should match snapshot and state 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
</Memo(Connect(TeamPermissionGate))>
|
</Memo(Connect(TeamPermissionGate))>
|
||||||
}
|
}
|
||||||
id="moreChannelsModal"
|
id="browseChannelsModal"
|
||||||
keyboardEscape={true}
|
keyboardEscape={true}
|
||||||
modalHeaderText={
|
modalHeaderText={
|
||||||
<Memo(MemoizedFormattedMessage)
|
<Memo(MemoizedFormattedMessage)
|
@ -1,6 +1,6 @@
|
|||||||
@charset 'UTF-8';
|
@charset 'UTF-8';
|
||||||
|
|
||||||
#moreChannelsModal {
|
#browseChannelsModal {
|
||||||
.modal-content {
|
.modal-content {
|
||||||
min-height: 600px;
|
min-height: 600px;
|
||||||
max-height: calc(50vh - 240px);
|
max-height: calc(50vh - 240px);
|
@ -5,8 +5,9 @@ import React from 'react';
|
|||||||
import {shallow} from 'enzyme';
|
import {shallow} from 'enzyme';
|
||||||
|
|
||||||
import {ActionResult} from 'mattermost-redux/types/actions';
|
import {ActionResult} from 'mattermost-redux/types/actions';
|
||||||
|
import {Channel} from '@mattermost/types/channels';
|
||||||
|
|
||||||
import MoreChannels, {Props} from 'components/more_channels/more_channels';
|
import BrowseChannels, {Props} from 'components/browse_channels/browse_channels';
|
||||||
import SearchableChannelList from 'components/searchable_channel_list';
|
import SearchableChannelList from 'components/searchable_channel_list';
|
||||||
|
|
||||||
import {getHistory} from 'utils/browser_history';
|
import {getHistory} from 'utils/browser_history';
|
||||||
@ -14,7 +15,7 @@ import {TestHelper} from 'utils/test_helper';
|
|||||||
|
|
||||||
jest.useFakeTimers('legacy');
|
jest.useFakeTimers('legacy');
|
||||||
|
|
||||||
describe('components/MoreChannels', () => {
|
describe('components/BrowseChannels', () => {
|
||||||
const searchResults = {
|
const searchResults = {
|
||||||
data: [{
|
data: [{
|
||||||
id: 'channel-id-1',
|
id: 'channel-id-1',
|
||||||
@ -29,6 +30,15 @@ describe('components/MoreChannels', () => {
|
|||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const archivedChannel = TestHelper.getChannelMock({
|
||||||
|
id: 'channel_id_2',
|
||||||
|
team_id: 'channel_team_2',
|
||||||
|
display_name: 'channel-2',
|
||||||
|
name: 'channel-2',
|
||||||
|
header: 'channel-2-header',
|
||||||
|
purpose: 'channel-2-purpose',
|
||||||
|
});
|
||||||
|
|
||||||
const channelActions = {
|
const channelActions = {
|
||||||
joinChannelAction: (userId: string, teamId: string, channelId: string): Promise<ActionResult> => {
|
joinChannelAction: (userId: string, teamId: string, channelId: string): Promise<ActionResult> => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
@ -56,18 +66,25 @@ describe('components/MoreChannels', () => {
|
|||||||
return resolve(searchResults);
|
return resolve(searchResults);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
getChannels: (): Promise<ActionResult<Channel[], Error>> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
return resolve({
|
||||||
|
data: [TestHelper.getChannelMock({})],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getArchivedChannels: (): Promise<ActionResult<Channel[], Error>> => {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
return resolve({
|
||||||
|
data: [archivedChannel],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseProps: Props = {
|
const baseProps: Props = {
|
||||||
channels: [TestHelper.getChannelMock({})],
|
channels: [TestHelper.getChannelMock({})],
|
||||||
archivedChannels: [TestHelper.getChannelMock({
|
archivedChannels: [archivedChannel],
|
||||||
id: 'channel_id_2',
|
|
||||||
team_id: 'channel_team_2',
|
|
||||||
display_name: 'channel-2',
|
|
||||||
name: 'channel-2',
|
|
||||||
header: 'channel-2-header',
|
|
||||||
purpose: 'channel-2-purpose',
|
|
||||||
})],
|
|
||||||
currentUserId: 'user-1',
|
currentUserId: 'user-1',
|
||||||
teamId: 'team_id',
|
teamId: 'team_id',
|
||||||
teamName: 'team_name',
|
teamName: 'team_name',
|
||||||
@ -76,20 +93,21 @@ describe('components/MoreChannels', () => {
|
|||||||
shouldHideJoinedChannels: false,
|
shouldHideJoinedChannels: false,
|
||||||
myChannelMemberships: {},
|
myChannelMemberships: {},
|
||||||
actions: {
|
actions: {
|
||||||
getChannels: jest.fn(),
|
getChannels: jest.fn(channelActions.getChannels),
|
||||||
getArchivedChannels: jest.fn(),
|
getArchivedChannels: jest.fn(channelActions.getArchivedChannels),
|
||||||
joinChannel: jest.fn(channelActions.joinChannelAction),
|
joinChannel: jest.fn(channelActions.joinChannelAction),
|
||||||
searchMoreChannels: jest.fn(channelActions.searchMoreChannels),
|
searchMoreChannels: jest.fn(channelActions.searchMoreChannels),
|
||||||
openModal: jest.fn(),
|
openModal: jest.fn(),
|
||||||
closeModal: jest.fn(),
|
closeModal: jest.fn(),
|
||||||
closeRightHandSide: jest.fn(),
|
closeRightHandSide: jest.fn(),
|
||||||
setGlobalItem: jest.fn(),
|
setGlobalItem: jest.fn(),
|
||||||
|
getChannelsMemberCount: jest.fn(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should match snapshot and state', () => {
|
test('should match snapshot and state', () => {
|
||||||
const wrapper = shallow<MoreChannels>(
|
const wrapper = shallow<BrowseChannels>(
|
||||||
<MoreChannels {...baseProps}/>,
|
<BrowseChannels {...baseProps}/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper).toMatchSnapshot();
|
expect(wrapper).toMatchSnapshot();
|
||||||
@ -105,8 +123,8 @@ describe('components/MoreChannels', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should call closeModal on handleExit', () => {
|
test('should call closeModal on handleExit', () => {
|
||||||
const wrapper = shallow<MoreChannels>(
|
const wrapper = shallow<BrowseChannels>(
|
||||||
<MoreChannels {...baseProps}/>,
|
<BrowseChannels {...baseProps}/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.instance().handleExit();
|
wrapper.instance().handleExit();
|
||||||
@ -114,8 +132,8 @@ describe('components/MoreChannels', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should match state on onChange', () => {
|
test('should match state on onChange', () => {
|
||||||
const wrapper = shallow<MoreChannels>(
|
const wrapper = shallow<BrowseChannels>(
|
||||||
<MoreChannels {...baseProps}/>,
|
<BrowseChannels {...baseProps}/>,
|
||||||
);
|
);
|
||||||
wrapper.setState({searchedChannels: [TestHelper.getChannelMock({id: 'other_channel_id'})]});
|
wrapper.setState({searchedChannels: [TestHelper.getChannelMock({id: 'other_channel_id'})]});
|
||||||
|
|
||||||
@ -129,8 +147,8 @@ describe('components/MoreChannels', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should call props.getChannels on nextPage', () => {
|
test('should call props.getChannels on nextPage', () => {
|
||||||
const wrapper = shallow<MoreChannels>(
|
const wrapper = shallow<BrowseChannels>(
|
||||||
<MoreChannels {...baseProps}/>,
|
<BrowseChannels {...baseProps}/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.instance().nextPage(1);
|
wrapper.instance().nextPage(1);
|
||||||
@ -141,7 +159,7 @@ describe('components/MoreChannels', () => {
|
|||||||
|
|
||||||
test('should have loading prop true when searching state is true', () => {
|
test('should have loading prop true when searching state is true', () => {
|
||||||
const wrapper = shallow(
|
const wrapper = shallow(
|
||||||
<MoreChannels {...baseProps}/>,
|
<BrowseChannels {...baseProps}/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.setState({search: true, searching: true});
|
wrapper.setState({search: true, searching: true});
|
||||||
@ -164,8 +182,8 @@ describe('components/MoreChannels', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow<MoreChannels>(
|
const wrapper = shallow<BrowseChannels>(
|
||||||
<MoreChannels {...props}/>,
|
<BrowseChannels {...props}/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
@ -192,8 +210,8 @@ describe('components/MoreChannels', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const wrapper = shallow<MoreChannels>(
|
const wrapper = shallow<BrowseChannels>(
|
||||||
<MoreChannels {...props}/>,
|
<BrowseChannels {...props}/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
@ -208,8 +226,8 @@ describe('components/MoreChannels', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should not perform a search if term is empty', () => {
|
test('should not perform a search if term is empty', () => {
|
||||||
const wrapper = shallow<MoreChannels>(
|
const wrapper = shallow<BrowseChannels>(
|
||||||
<MoreChannels {...baseProps}/>,
|
<BrowseChannels {...baseProps}/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.instance().onChange = jest.fn();
|
wrapper.instance().onChange = jest.fn();
|
||||||
@ -223,8 +241,8 @@ describe('components/MoreChannels', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should handle a failed search', (done) => {
|
test('should handle a failed search', (done) => {
|
||||||
const wrapper = shallow<MoreChannels>(
|
const wrapper = shallow<BrowseChannels>(
|
||||||
<MoreChannels {...baseProps}/>,
|
<BrowseChannels {...baseProps}/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.instance().onChange = jest.fn();
|
wrapper.instance().onChange = jest.fn();
|
||||||
@ -251,8 +269,8 @@ describe('components/MoreChannels', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should perform search and set the correct state', (done) => {
|
test('should perform search and set the correct state', (done) => {
|
||||||
const wrapper = shallow<MoreChannels>(
|
const wrapper = shallow<BrowseChannels>(
|
||||||
<MoreChannels {...baseProps}/>,
|
<BrowseChannels {...baseProps}/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.instance().onChange = jest.fn();
|
wrapper.instance().onChange = jest.fn();
|
||||||
@ -277,8 +295,8 @@ describe('components/MoreChannels', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should perform search on archived channels and set the correct state', (done) => {
|
test('should perform search on archived channels and set the correct state', (done) => {
|
||||||
const wrapper = shallow<MoreChannels>(
|
const wrapper = shallow<BrowseChannels>(
|
||||||
<MoreChannels {...baseProps}/>,
|
<BrowseChannels {...baseProps}/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
wrapper.instance().onChange = jest.fn();
|
wrapper.instance().onChange = jest.fn();
|
@ -24,15 +24,15 @@ import classNames from 'classnames';
|
|||||||
import {localizeMessage} from 'utils/utils';
|
import {localizeMessage} from 'utils/utils';
|
||||||
import LoadingScreen from 'components/loading_screen';
|
import LoadingScreen from 'components/loading_screen';
|
||||||
|
|
||||||
import './more_channels.scss';
|
import './browse_channels.scss';
|
||||||
|
|
||||||
const CHANNELS_CHUNK_SIZE = 50;
|
const CHANNELS_CHUNK_SIZE = 50;
|
||||||
const CHANNELS_PER_PAGE = 50;
|
const CHANNELS_PER_PAGE = 50;
|
||||||
const SEARCH_TIMEOUT_MILLISECONDS = 100;
|
const SEARCH_TIMEOUT_MILLISECONDS = 100;
|
||||||
|
|
||||||
type Actions = {
|
type Actions = {
|
||||||
getChannels: (teamId: string, page: number, perPage: number) => void;
|
getChannels: (teamId: string, page: number, perPage: number) => Promise<ActionResult<Channel[], Error>>;
|
||||||
getArchivedChannels: (teamId: string, page: number, channelsPerPage: number) => void;
|
getArchivedChannels: (teamId: string, page: number, channelsPerPage: number) => Promise<ActionResult<Channel[], Error>>;
|
||||||
joinChannel: (currentUserId: string, teamId: string, channelId: string) => Promise<ActionResult>;
|
joinChannel: (currentUserId: string, teamId: string, channelId: string) => Promise<ActionResult>;
|
||||||
searchMoreChannels: (term: string, shouldShowArchivedChannels: boolean, shouldHideJoinedChannels: boolean) => Promise<ActionResult>;
|
searchMoreChannels: (term: string, shouldShowArchivedChannels: boolean, shouldHideJoinedChannels: boolean) => Promise<ActionResult>;
|
||||||
openModal: <P>(modalData: ModalData<P>) => void;
|
openModal: <P>(modalData: ModalData<P>) => void;
|
||||||
@ -43,6 +43,7 @@ type Actions = {
|
|||||||
*/
|
*/
|
||||||
setGlobalItem: (name: string, value: string) => void;
|
setGlobalItem: (name: string, value: string) => void;
|
||||||
closeRightHandSide: () => void;
|
closeRightHandSide: () => void;
|
||||||
|
getChannelsMemberCount: (channelIds: string[]) => Promise<ActionResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
@ -58,6 +59,7 @@ export type Props = {
|
|||||||
shouldHideJoinedChannels: boolean;
|
shouldHideJoinedChannels: boolean;
|
||||||
rhsState?: RhsState;
|
rhsState?: RhsState;
|
||||||
rhsOpen?: boolean;
|
rhsOpen?: boolean;
|
||||||
|
channelsMemberCount?: Record<string, number>;
|
||||||
actions: Actions;
|
actions: Actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +73,7 @@ type State = {
|
|||||||
searchTerm: string;
|
searchTerm: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class MoreChannels extends React.PureComponent<Props, State> {
|
export default class BrowseChannels extends React.PureComponent<Props, State> {
|
||||||
public searchTimeoutId: number;
|
public searchTimeoutId: number;
|
||||||
activeChannels: Channel[] = [];
|
activeChannels: Channel[] = [];
|
||||||
|
|
||||||
@ -92,10 +94,23 @@ export default class MoreChannels extends React.PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.props.actions.getChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2);
|
const promises = [
|
||||||
|
this.props.actions.getChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2),
|
||||||
|
];
|
||||||
|
|
||||||
if (this.props.canShowArchivedChannels) {
|
if (this.props.canShowArchivedChannels) {
|
||||||
this.props.actions.getArchivedChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2);
|
promises.push(this.props.actions.getArchivedChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Promise.all(promises).then((results) => {
|
||||||
|
const channelIDsForMemberCount = results.flatMap((result) => {
|
||||||
|
return result.data ? result.data.map((channel) => channel.id) : [];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (channelIDsForMemberCount.length > 0) {
|
||||||
|
this.props.actions.getChannelsMemberCount(channelIDsForMemberCount);
|
||||||
|
}
|
||||||
|
});
|
||||||
this.loadComplete();
|
this.loadComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +149,11 @@ export default class MoreChannels extends React.PureComponent<Props, State> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
nextPage = (page: number) => {
|
nextPage = (page: number) => {
|
||||||
this.props.actions.getChannels(this.props.teamId, page + 1, CHANNELS_PER_PAGE);
|
this.props.actions.getChannels(this.props.teamId, page + 1, CHANNELS_PER_PAGE).then((result) => {
|
||||||
|
if (result.data && result.data.length > 0) {
|
||||||
|
this.props.actions.getChannelsMemberCount(result.data.map((channel) => channel.id));
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleJoin = async (channel: Channel, done: () => void) => {
|
handleJoin = async (channel: Channel, done: () => void) => {
|
||||||
@ -148,6 +167,7 @@ export default class MoreChannels extends React.PureComponent<Props, State> {
|
|||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
this.setState({serverError: result.error.message});
|
this.setState({serverError: result.error.message});
|
||||||
} else {
|
} else {
|
||||||
|
this.props.actions.getChannelsMemberCount([channel.id]);
|
||||||
getHistory().push(getRelativeChannelURL(teamName, channel.name));
|
getHistory().push(getRelativeChannelURL(teamName, channel.name));
|
||||||
this.closeEditRHS();
|
this.closeEditRHS();
|
||||||
}
|
}
|
||||||
@ -211,9 +231,6 @@ export default class MoreChannels extends React.PureComponent<Props, State> {
|
|||||||
this.props.actions.setGlobalItem(StoragePrefixes.HIDE_JOINED_CHANNELS, shouldHideJoinedChannels.toString());
|
this.props.actions.setGlobalItem(StoragePrefixes.HIDE_JOINED_CHANNELS, shouldHideJoinedChannels.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
otherChannelsWithoutJoined = this.props.channels.filter((channel) => !this.isMemberOfChannel(channel.id));
|
|
||||||
archivedChannelsWithoutJoined = this.props.archivedChannels.filter((channel) => !this.isMemberOfChannel(channel.id));
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
channels,
|
channels,
|
||||||
@ -304,6 +321,7 @@ export default class MoreChannels extends React.PureComponent<Props, State> {
|
|||||||
closeModal={this.props.actions.closeModal}
|
closeModal={this.props.actions.closeModal}
|
||||||
hideJoinedChannelsPreference={this.handleShowJoinedChannelsPreference}
|
hideJoinedChannelsPreference={this.handleShowJoinedChannelsPreference}
|
||||||
rememberHideJoinedChannelsChecked={shouldHideJoinedChannels}
|
rememberHideJoinedChannelsChecked={shouldHideJoinedChannels}
|
||||||
|
channelsMemberCount={this.props.channelsMemberCount}
|
||||||
/>
|
/>
|
||||||
{serverError}
|
{serverError}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
@ -319,8 +337,8 @@ export default class MoreChannels extends React.PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<GenericModal
|
<GenericModal
|
||||||
onExited={this.handleExit}
|
onExited={this.handleExit}
|
||||||
id='moreChannelsModal'
|
id='browseChannelsModal'
|
||||||
aria-labelledby='moreChannelsModalLabel'
|
aria-labelledby='browseChannelsModalLabel'
|
||||||
compassDesign={true}
|
compassDesign={true}
|
||||||
modalHeaderText={title}
|
modalHeaderText={title}
|
||||||
headerButton={createNewChannelButton('outlineButton')}
|
headerButton={createNewChannelButton('outlineButton')}
|
@ -11,8 +11,8 @@ import {getConfig} from 'mattermost-redux/selectors/entities/general';
|
|||||||
import {Action, ActionResult} from 'mattermost-redux/types/actions';
|
import {Action, ActionResult} from 'mattermost-redux/types/actions';
|
||||||
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
|
||||||
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
|
||||||
import {getChannels, getArchivedChannels, joinChannel} from 'mattermost-redux/actions/channels';
|
import {getChannels, getArchivedChannels, joinChannel, getChannelsMemberCount} from 'mattermost-redux/actions/channels';
|
||||||
import {getChannelsInCurrentTeam, getMyChannelMemberships} from 'mattermost-redux/selectors/entities/channels';
|
import {getChannelsInCurrentTeam, getMyChannelMemberships, getChannelsMemberCount as getChannelsMemberCountSelector} from 'mattermost-redux/selectors/entities/channels';
|
||||||
|
|
||||||
import {searchMoreChannels} from 'actions/channel_actions';
|
import {searchMoreChannels} from 'actions/channel_actions';
|
||||||
import {openModal, closeModal} from 'actions/views/modals';
|
import {openModal, closeModal} from 'actions/views/modals';
|
||||||
@ -23,7 +23,7 @@ import {getIsRhsOpen, getRhsState} from 'selectors/rhs';
|
|||||||
import {ModalData} from 'types/actions';
|
import {ModalData} from 'types/actions';
|
||||||
import {GlobalState} from 'types/store';
|
import {GlobalState} from 'types/store';
|
||||||
|
|
||||||
import MoreChannels from './more_channels';
|
import BrowseChannels from './browse_channels';
|
||||||
import {makeGetGlobalItem} from 'selectors/storage';
|
import {makeGetGlobalItem} from 'selectors/storage';
|
||||||
import Constants, {StoragePrefixes} from 'utils/constants';
|
import Constants, {StoragePrefixes} from 'utils/constants';
|
||||||
import {setGlobalItem} from 'actions/storage';
|
import {setGlobalItem} from 'actions/storage';
|
||||||
@ -56,18 +56,20 @@ function mapStateToProps(state: GlobalState) {
|
|||||||
shouldHideJoinedChannels: getGlobalItem(state) === 'true',
|
shouldHideJoinedChannels: getGlobalItem(state) === 'true',
|
||||||
rhsState: getRhsState(state),
|
rhsState: getRhsState(state),
|
||||||
rhsOpen: getIsRhsOpen(state),
|
rhsOpen: getIsRhsOpen(state),
|
||||||
|
channelsMemberCount: getChannelsMemberCountSelector(state),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
type Actions = {
|
type Actions = {
|
||||||
getChannels: (teamId: string, page: number, perPage: number) => void;
|
getChannels: (teamId: string, page: number, perPage: number) => Promise<ActionResult<Channel[], Error>>;
|
||||||
getArchivedChannels: (teamId: string, page: number, channelsPerPage: number) => void;
|
getArchivedChannels: (teamId: string, page: number, channelsPerPage: number) => Promise<ActionResult<Channel[], Error>>;
|
||||||
joinChannel: (currentUserId: string, teamId: string, channelId: string) => Promise<ActionResult>;
|
joinChannel: (currentUserId: string, teamId: string, channelId: string) => Promise<ActionResult>;
|
||||||
searchMoreChannels: (term: string, shouldShowArchivedChannels: boolean) => Promise<ActionResult>;
|
searchMoreChannels: (term: string, shouldShowArchivedChannels: boolean) => Promise<ActionResult>;
|
||||||
openModal: <P>(modalData: ModalData<P>) => void;
|
openModal: <P>(modalData: ModalData<P>) => void;
|
||||||
closeModal: (modalId: string) => void;
|
closeModal: (modalId: string) => void;
|
||||||
setGlobalItem: (name: string, value: string) => void;
|
setGlobalItem: (name: string, value: string) => void;
|
||||||
closeRightHandSide: () => void;
|
closeRightHandSide: () => void;
|
||||||
|
getChannelsMemberCount: (channelIds: string[]) => Promise<ActionResult>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: Dispatch) {
|
function mapDispatchToProps(dispatch: Dispatch) {
|
||||||
@ -81,8 +83,9 @@ function mapDispatchToProps(dispatch: Dispatch) {
|
|||||||
closeModal,
|
closeModal,
|
||||||
setGlobalItem,
|
setGlobalItem,
|
||||||
closeRightHandSide,
|
closeRightHandSide,
|
||||||
|
getChannelsMemberCount,
|
||||||
}, dispatch),
|
}, dispatch),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(MoreChannels);
|
export default connect(mapStateToProps, mapDispatchToProps)(BrowseChannels);
|
@ -4,7 +4,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
import {ArchiveOutlineIcon, CheckIcon, ChevronDownIcon, GlobeIcon, LockOutlineIcon, MagnifyIcon} from '@mattermost/compass-icons/components';
|
import {ArchiveOutlineIcon, CheckIcon, ChevronDownIcon, GlobeIcon, LockOutlineIcon, MagnifyIcon, AccountOutlineIcon} from '@mattermost/compass-icons/components';
|
||||||
import {Channel, ChannelMembership} from '@mattermost/types/channels';
|
import {Channel, ChannelMembership} from '@mattermost/types/channels';
|
||||||
import {RelationOneToOne} from '@mattermost/types/utilities';
|
import {RelationOneToOne} from '@mattermost/types/utilities';
|
||||||
import {isPrivateChannel} from 'mattermost-redux/utils/channel_utils';
|
import {isPrivateChannel} from 'mattermost-redux/utils/channel_utils';
|
||||||
@ -46,6 +46,7 @@ type Props = {
|
|||||||
rememberHideJoinedChannelsChecked: boolean;
|
rememberHideJoinedChannelsChecked: boolean;
|
||||||
canShowArchivedChannels?: boolean;
|
canShowArchivedChannels?: boolean;
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
channelsMemberCount?: Record<string, number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
@ -133,6 +134,10 @@ export default class SearchableChannelList extends React.PureComponent<Props, St
|
|||||||
} else {
|
} else {
|
||||||
channelTypeIcon = <GlobeIcon size={18}/>;
|
channelTypeIcon = <GlobeIcon size={18}/>;
|
||||||
}
|
}
|
||||||
|
let memberCount = 0;
|
||||||
|
if (this.props.channelsMemberCount?.[channel.id]) {
|
||||||
|
memberCount = this.props.channelsMemberCount[channel.id];
|
||||||
|
}
|
||||||
|
|
||||||
const membershipIndicator = this.isMemberOfChannel(channel.id) ? (
|
const membershipIndicator = this.isMemberOfChannel(channel.id) ? (
|
||||||
<div
|
<div
|
||||||
@ -149,8 +154,8 @@ export default class SearchableChannelList extends React.PureComponent<Props, St
|
|||||||
|
|
||||||
const channelPurposeContainerAriaLabel = localizeAndFormatMessage(
|
const channelPurposeContainerAriaLabel = localizeAndFormatMessage(
|
||||||
t('more_channels.channel_purpose'),
|
t('more_channels.channel_purpose'),
|
||||||
'Channel Information: Membership Indicator: Joined, Purpose: {channelPurpose}',
|
'Channel Information: Membership Indicator: Joined, Member count {memberCount}, Purpose: {channelPurpose}',
|
||||||
{channelPurpose: channel.purpose || ''},
|
{memberCount, channelPurpose: channel.purpose || ''},
|
||||||
);
|
);
|
||||||
|
|
||||||
const channelPurposeContainer = (
|
const channelPurposeContainer = (
|
||||||
@ -159,7 +164,10 @@ export default class SearchableChannelList extends React.PureComponent<Props, St
|
|||||||
aria-label={channelPurposeContainerAriaLabel}
|
aria-label={channelPurposeContainerAriaLabel}
|
||||||
>
|
>
|
||||||
{membershipIndicator}
|
{membershipIndicator}
|
||||||
{(channel.purpose.length > 0 && membershipIndicator) ? <span className='dot'/> : null}
|
{membershipIndicator ? <span className='dot'/> : null}
|
||||||
|
<AccountOutlineIcon size={14}/>
|
||||||
|
<span data-testid={`channelMemberCount-${channel.name}`} >{memberCount}</span>
|
||||||
|
{channel.purpose.length > 0 ? <span className='dot'/> : null}
|
||||||
<span className='more-modal__description'>{channel.purpose}</span>
|
<span className='more-modal__description'>{channel.purpose}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -195,6 +203,7 @@ export default class SearchableChannelList extends React.PureComponent<Props, St
|
|||||||
className='more-modal__row'
|
className='more-modal__row'
|
||||||
key={channel.id}
|
key={channel.id}
|
||||||
id={`ChannelRow-${channel.name}`}
|
id={`ChannelRow-${channel.name}`}
|
||||||
|
data-testid={`ChannelRow-${channel.name}`}
|
||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
onClick={(e) => this.handleJoin(channel, e)}
|
onClick={(e) => this.handleJoin(channel, e)}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
|
@ -9,7 +9,7 @@ import {useSelector, useDispatch} from 'react-redux';
|
|||||||
|
|
||||||
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
import MenuWrapper from 'components/widgets/menu/menu_wrapper';
|
||||||
import Menu from 'components/widgets/menu/menu';
|
import Menu from 'components/widgets/menu/menu';
|
||||||
import MoreChannels from 'components/more_channels';
|
import BrowseChannels from 'components/browse_channels';
|
||||||
import NewChannelModal from 'components/new_channel_modal/new_channel_modal';
|
import NewChannelModal from 'components/new_channel_modal/new_channel_modal';
|
||||||
|
|
||||||
import {isAddChannelCtaDropdownOpen} from 'selectors/views/add_channel_dropdown';
|
import {isAddChannelCtaDropdownOpen} from 'selectors/views/add_channel_dropdown';
|
||||||
@ -59,7 +59,7 @@ const AddChannelsCtaButton = (): JSX.Element | null => {
|
|||||||
const showMoreChannelsModal = () => {
|
const showMoreChannelsModal = () => {
|
||||||
dispatch(openModal({
|
dispatch(openModal({
|
||||||
modalId: ModalIdentifiers.MORE_CHANNELS,
|
modalId: ModalIdentifiers.MORE_CHANNELS,
|
||||||
dialogType: MoreChannels,
|
dialogType: BrowseChannels,
|
||||||
dialogProps: {morePublicChannelsModalType: 'public'},
|
dialogProps: {morePublicChannelsModalType: 'public'},
|
||||||
}));
|
}));
|
||||||
trackEvent('ui', 'browse_channels_button_is_clicked');
|
trackEvent('ui', 'browse_channels_button_is_clicked');
|
||||||
|
@ -8,7 +8,7 @@ import {trackEvent} from 'actions/telemetry_actions';
|
|||||||
import EditCategoryModal from 'components/edit_category_modal';
|
import EditCategoryModal from 'components/edit_category_modal';
|
||||||
import MoreDirectChannels from 'components/more_direct_channels';
|
import MoreDirectChannels from 'components/more_direct_channels';
|
||||||
import DataPrefetch from 'components/data_prefetch';
|
import DataPrefetch from 'components/data_prefetch';
|
||||||
import MoreChannels from 'components/more_channels';
|
import BrowseChannels from 'components/browse_channels';
|
||||||
import NewChannelModal from 'components/new_channel_modal/new_channel_modal';
|
import NewChannelModal from 'components/new_channel_modal/new_channel_modal';
|
||||||
import InvitationModal from 'components/invitation_modal';
|
import InvitationModal from 'components/invitation_modal';
|
||||||
import UserSettingsModal from 'components/user_settings/modal';
|
import UserSettingsModal from 'components/user_settings/modal';
|
||||||
@ -155,7 +155,7 @@ export default class Sidebar extends React.PureComponent<Props, State> {
|
|||||||
showMoreChannelsModal = () => {
|
showMoreChannelsModal = () => {
|
||||||
this.props.actions.openModal({
|
this.props.actions.openModal({
|
||||||
modalId: ModalIdentifiers.MORE_CHANNELS,
|
modalId: ModalIdentifiers.MORE_CHANNELS,
|
||||||
dialogType: MoreChannels,
|
dialogType: BrowseChannels,
|
||||||
dialogProps: {morePublicChannelsModalType: 'public'},
|
dialogProps: {morePublicChannelsModalType: 'public'},
|
||||||
});
|
});
|
||||||
trackEvent('ui', 'ui_channels_more_public_v2');
|
trackEvent('ui', 'ui_channels_more_public_v2');
|
||||||
|
@ -56,6 +56,7 @@ export default keyMirror({
|
|||||||
RECEIVED_CHANNEL_MEMBERS: null,
|
RECEIVED_CHANNEL_MEMBERS: null,
|
||||||
RECEIVED_CHANNEL_MEMBER: null,
|
RECEIVED_CHANNEL_MEMBER: null,
|
||||||
RECEIVED_CHANNEL_STATS: null,
|
RECEIVED_CHANNEL_STATS: null,
|
||||||
|
RECEIVED_CHANNELS_MEMBER_COUNT: null,
|
||||||
RECEIVED_CHANNEL_PROPS: null,
|
RECEIVED_CHANNEL_PROPS: null,
|
||||||
RECEIVED_CHANNEL_DELETED: null,
|
RECEIVED_CHANNEL_DELETED: null,
|
||||||
RECEIVED_CHANNEL_UNARCHIVED: null,
|
RECEIVED_CHANNEL_UNARCHIVED: null,
|
||||||
|
@ -1083,6 +1083,27 @@ export function getChannelStats(channelId: string, excludeFilesCount?: boolean):
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getChannelsMemberCount(channelIds: string[]): ActionFunc {
|
||||||
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
|
let channelsMemberCount;
|
||||||
|
|
||||||
|
try {
|
||||||
|
channelsMemberCount = await Client4.getChannelsMemberCount(channelIds);
|
||||||
|
} catch (error) {
|
||||||
|
forceLogoutIfNecessary(error, dispatch, getState);
|
||||||
|
dispatch(logError(error));
|
||||||
|
return {error};
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch({
|
||||||
|
type: ChannelTypes.RECEIVED_CHANNELS_MEMBER_COUNT,
|
||||||
|
data: channelsMemberCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {data: channelsMemberCount};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function addChannelMember(channelId: string, userId: string, postRootId = ''): ActionFunc {
|
export function addChannelMember(channelId: string, userId: string, postRootId = ''): ActionFunc {
|
||||||
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
return async (dispatch: DispatchFunc, getState: GetStateFunc) => {
|
||||||
let member;
|
let member;
|
||||||
|
@ -713,6 +713,22 @@ function stats(state: RelationOneToOne<Channel, ChannelStats> = {}, action: Gene
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function channelsMemberCount(state: Record<string, number> = {}, action: GenericAction) {
|
||||||
|
switch (action.type) {
|
||||||
|
case ChannelTypes.RECEIVED_CHANNELS_MEMBER_COUNT: {
|
||||||
|
const memberCount = action.data;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
...memberCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case UserTypes.LOGOUT_SUCCESS:
|
||||||
|
return {};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function groupsAssociatedToChannel(state: any = {}, action: GenericAction) {
|
function groupsAssociatedToChannel(state: any = {}, action: GenericAction) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNELS_IN_TEAM: {
|
case GroupTypes.RECEIVED_ALL_GROUPS_ASSOCIATED_TO_CHANNELS_IN_TEAM: {
|
||||||
@ -989,4 +1005,7 @@ export default combineReducers({
|
|||||||
|
|
||||||
// object where every key is the channel id mapping to an object containing the number of messages in the channel
|
// object where every key is the channel id mapping to an object containing the number of messages in the channel
|
||||||
messageCounts,
|
messageCounts,
|
||||||
|
|
||||||
|
// object where key is the channel id and value is the member count for the channel
|
||||||
|
channelsMemberCount,
|
||||||
});
|
});
|
||||||
|
@ -98,6 +98,10 @@ export function getAllChannelStats(state: GlobalState): RelationOneToOne<Channel
|
|||||||
return state.entities.channels.stats;
|
return state.entities.channels.stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getChannelsMemberCount(state: GlobalState): Record<string, number> {
|
||||||
|
return state.entities.channels.channelsMemberCount;
|
||||||
|
}
|
||||||
|
|
||||||
export function getChannelsInTeam(state: GlobalState): RelationOneToMany<Team, Channel> {
|
export function getChannelsInTeam(state: GlobalState): RelationOneToMany<Team, Channel> {
|
||||||
return state.entities.channels.channelsInTeam;
|
return state.entities.channels.channelsInTeam;
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,7 @@ const state: GlobalState = {
|
|||||||
channelModerations: {},
|
channelModerations: {},
|
||||||
channelMemberCountsByGroup: {},
|
channelMemberCountsByGroup: {},
|
||||||
messageCounts: {},
|
messageCounts: {},
|
||||||
|
channelsMemberCount: {},
|
||||||
},
|
},
|
||||||
posts: {
|
posts: {
|
||||||
expandedURLs: {},
|
expandedURLs: {},
|
||||||
|
@ -1755,6 +1755,13 @@ export default class Client4 {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getChannelsMemberCount = (channelIds: string[]) => {
|
||||||
|
return this.doFetch<Record<string, number>>(
|
||||||
|
`${this.getChannelsRoute()}/stats/member_count`,
|
||||||
|
{method: 'post', body: JSON.stringify(channelIds)}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
getChannelModerations = (channelId: string) => {
|
getChannelModerations = (channelId: string) => {
|
||||||
return this.doFetch<ChannelModeration[]>(
|
return this.doFetch<ChannelModeration[]>(
|
||||||
`${this.getChannelRoute(channelId)}/moderations`,
|
`${this.getChannelRoute(channelId)}/moderations`,
|
||||||
|
@ -156,6 +156,7 @@ export type ChannelsState = {
|
|||||||
channelModerations: RelationOneToOne<Channel, ChannelModeration[]>;
|
channelModerations: RelationOneToOne<Channel, ChannelModeration[]>;
|
||||||
channelMemberCountsByGroup: RelationOneToOne<Channel, ChannelMemberCountsByGroup>;
|
channelMemberCountsByGroup: RelationOneToOne<Channel, ChannelMemberCountsByGroup>;
|
||||||
messageCounts: RelationOneToOne<Channel, ChannelMessageCount>;
|
messageCounts: RelationOneToOne<Channel, ChannelMessageCount>;
|
||||||
|
channelsMemberCount: Record<string, number>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChannelModeration = {
|
export type ChannelModeration = {
|
||||||
|
Loading…
Reference in New Issue
Block a user