VYPR
Low severityNVD Advisory· Published Nov 14, 2025· Updated Nov 14, 2025

Unauthorized access to archived channel content via threads interface

CVE-2025-41436

Description

Mattermost versions <11.0 fail to properly enforce the "Allow users to view archived channels" setting which allows regular users to access archived channel content and files via the "Open in Channel" functionality from followed threads

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/mattermost/mattermost-serverGo
< 11.0.0-alpha.111.0.0-alpha.1
github.com/mattermost/mattermost/server/v8Go
< 8.0.0-20250815165020-c8d66301415d8.0.0-20250815165020-c8d66301415d

Affected products

1

Patches

1
c8d66301415d

MM-63240: Always allow viewing archived channels (#32162)

https://github.com/mattermost/mattermostJesse HallamAug 15, 2025via ghsa
81 files changed · +446 1691
  • e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_add_reaction_spec.ts+0 6 modified
    @@ -14,12 +14,6 @@ import * as TIMEOUTS from '../../../fixtures/timeouts';
     
     describe('Archived channels', () => {
         before(() => {
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             // # Login as test user and visit created channel
             cy.apiInitSetup({loginAfter: true}).then(({team, channel}) => {
                 cy.visit(`/${team.name}/channels/${channel.name}`);
    
  • e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_header_spec.ts+0 6 modified
    @@ -29,12 +29,6 @@ describe('Archive channel header spec', () => {
             cy.uiSave();
             cy.uiSaveButton().should('be.visible');
     
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             // # Login as test user and visit create channel
             cy.apiInitSetup({loginAfter: true}).then(({channelUrl}) => {
                 cy.visit(channelUrl);
    
  • e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_member_spec.ts+0 6 modified
    @@ -12,12 +12,6 @@
     
     describe('Archive channel members spec', () => {
         before(() => {
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             // # Login as test user and visit create channel
             cy.apiInitSetup({loginAfter: true, promoteNewUserAsAdmin: true}).then(({team, channel}) => {
                 cy.visit(`/${team.name}/channels/${channel.name}`);
    
  • e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_operations_spec.ts+0 128 removed
    @@ -1,128 +0,0 @@
    -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
    -// See LICENSE.txt for license information.
    -
    -// ***************************************************************
    -// - [#] indicates a test step (e.g. # Go to a page)
    -// - [*] indicates an assertion (e.g. * Check the title)
    -// - Use element ID when selecting an element. Create one if none.
    -// ***************************************************************
    -
    -// Group: @channels @channel
    -
    -import {getRandomId} from '../../../utils';
    -
    -import {createArchivedChannel} from './helpers';
    -
    -describe('Leave an archived channel', () => {
    -    let testTeam;
    -    let testChannel;
    -    let testUser;
    -    const testArchivedMessage = `this is an archived post ${getRandomId()}`;
    -
    -    before(() => {
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
    -        // # Login as test user and visit test channel
    -        cy.apiInitSetup().then(({team, channel, user}) => {
    -            testTeam = team;
    -            testChannel = channel;
    -            testUser = user;
    -
    -            cy.apiCreateUser({prefix: 'second'}).then(({user: second}) => {
    -                cy.apiAddUserToTeam(testTeam.id, second.id);
    -            });
    -            cy.visit(`/${team.name}/channels/${testChannel.name}`);
    -            cy.postMessageAs({sender: testUser, message: testArchivedMessage, channelId: testChannel.id});
    -        });
    -    });
    -
    -    it('MM-T1704 Archived channels appear in channel switcher after refresh', () => {
    -        // # Archive the channel
    -        cy.apiLogin(testUser);
    -        cy.uiArchiveChannel();
    -
    -        // # Switch to another channel
    -        cy.visit(`/${testTeam.name}/channels/off-topic`);
    -
    -        // # Use CTRL / CMD+K shortcut to open channel switcher.
    -        cy.typeCmdOrCtrl().type('K', {release: true});
    -
    -        // # Start typing the name of the archived channel in the search bar of the channel switcher
    -        cy.get('#quickSwitchInput').type(testChannel.display_name);
    -
    -        // * The archived channel appears in channel switcher search results
    -        cy.get('#suggestionList').should('be.visible');
    -        cy.get('#suggestionList').find(`#suggestionList_item_${testChannel.id}`).should('be.visible');
    -
    -        // # Reload the app (refresh the web page)
    -        cy.reload().then(() => {
    -            // # Return to the channel switcher
    -            cy.typeCmdOrCtrl().type('K', {release: true});
    -
    -            // # Start typing the name of the archived channel in the search bar of the channel switcher
    -            cy.get('#quickSwitchInput').type(testChannel.display_name).then(() => {
    -                // * The archived channel appears in channel switcher search results
    -                cy.get('#suggestionList').should('be.visible');
    -                cy.get('#suggestionList').find(`#suggestionList_item_${testChannel.id}`).should('be.visible');
    -            });
    -        });
    -    });
    -
    -    it('MM-T1705 User can unarchive a public channel', () => {
    -        // # As a user with appropriate permission, archive a public channel:
    -        cy.apiAdminLogin();
    -
    -        cy.visit(`/${testTeam.name}/channels/off-topic`);
    -        cy.contains('#channelHeaderTitle', 'Off-Topic');
    -
    -        const messageText = `archived text ${getRandomId()}`;
    -
    -        createArchivedChannel({prefix: 'unarchive-'}, [messageText]).then(({name}) => {
    -            // # View the archived channel, noting that it is read-only
    -            cy.uiGetPostTextBox({exist: false});
    -
    -            // # Unarchive the channel:
    -            cy.uiUnarchiveChannel().then(() => {
    -                // * Channel is no longer read-only
    -                cy.uiGetPostTextBox();
    -
    -                // * Channel is displayed in LHS with the normal icon, not an archived channel icon
    -                cy.get(`#sidebarItem_${name}`).scrollIntoView().should('be.visible');
    -
    -                cy.get(`#sidebarItem_${name}`).find('.icon-globe').should('be.visible');
    -            });
    -        });
    -    });
    -
    -    it('MM-T1706 User can unarchive a private channel', () => {
    -        // # As a user with appropriate permission, archive a private channel:
    -        cy.apiAdminLogin();
    -
    -        cy.visit(`/${testTeam.name}/channels/off-topic`);
    -        cy.contains('#channelHeaderTitle', 'Off-Topic');
    -
    -        const messageText = `archived text ${getRandomId()}`;
    -        const channelOptions = {
    -            prefix: 'private-unarchive-',
    -            isPrivate: true,
    -        };
    -
    -        createArchivedChannel(channelOptions, [messageText]).then(({name}) => {
    -            // # View the archived channel, noting that it is read-only
    -            cy.uiGetPostTextBox({exist: false});
    -
    -            // # Unarchive the channel:
    -            cy.uiUnarchiveChannel().then(() => {
    -                // * Channel is no longer read-only
    -                cy.uiGetPostTextBox();
    -
    -                // * Channel is displayed in LHS with the normal icon, not an archived channel icon
    -                cy.get(`#sidebarItem_${name}`).find('.icon-lock-outline').should('be.visible');
    -            });
    -        });
    -    });
    -});
    
  • e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_post_spec.ts+0 6 modified
    @@ -18,12 +18,6 @@ describe('Archived channels', () => {
         let otherUser;
     
         before(() => {
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             // # Login as test user and visit created channel
             cy.apiInitSetup().then(({team, user}) => {
                 testUser = user;
    
  • e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_reaction_spec.ts+0 6 modified
    @@ -14,12 +14,6 @@ import * as TIMEOUTS from '../../../fixtures/timeouts';
     
     describe('Archived channels', () => {
         before(() => {
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             // # Login as test user and visit created channel
             cy.apiInitSetup({loginAfter: true}).then(({team, channel}) => {
                 cy.visit(`/${team.name}/channels/${channel.name}`);
    
  • e2e-tests/cypress/tests/integration/channels/archived_channel/archive_channel_search_spec.ts+8 26 modified
    @@ -14,19 +14,13 @@ import {getRandomId} from '../../../utils';
     
     import {createArchivedChannel} from './helpers';
     
    -describe('archive tests while preventing viewing archived channels', () => {
    +describe('archive channel search tests', () => {
         let testTeam;
         let testChannel;
         let testUser;
         const testArchivedMessage = `this is an archived post ${getRandomId()}`;
     
         before(() => {
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             // # Login as test user and visit test channel
             cy.apiInitSetup().then(({team, channel, user}) => {
                 testTeam = team;
    @@ -48,13 +42,6 @@ describe('archive tests while preventing viewing archived channels', () => {
             createArchivedChannel({prefix: 'pineapple-'}, [messageText]).then(() => {
                 // # Unarchive the channel
                 cy.uiUnarchiveChannel().then(() => {
    -                // # Remove search on archived channels
    -                cy.apiUpdateConfig({
    -                    TeamSettings: {
    -                        ExperimentalViewArchivedChannels: false,
    -                    },
    -                });
    -
                     cy.visit(`/${testTeam.name}/channels/off-topic`);
                     cy.contains('#channelHeaderTitle', 'Off-Topic');
                     cy.postMessage(getRandomId());
    @@ -70,14 +57,7 @@ describe('archive tests while preventing viewing archived channels', () => {
             });
         });
     
    -    it('MM-T1708 An archived channel can\'t be searched when "Allow users to view archived channels" is set to False in "Site Configuration > Users and Teams" in the System Console', () => {
    -        // # First, as system admin, ensure that System Console > Users and Teams > Allow users to view archived channels is set to `false`.
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: false,
    -            },
    -        });
    -
    +    it('MM-T1708 An archived channel can be searched since archived channels are always viewable', () => {
             cy.apiLogin(testUser);
     
             // # Open a channel other than Town Square
    @@ -94,13 +74,13 @@ describe('archive tests while preventing viewing archived channels', () => {
             // # Select Archive Channel from the header menu
             cy.uiArchiveChannel();
     
    -        // # Archive dialogue message reads "This will archive the channel from the team and make its contents inaccessible for all users" (Mobile dialogue makes no mention of the data will be accessible)
    +        // # Search for the archived message
             cy.uiGetSearchContainer().click();
             cy.uiGetSearchBox().clear().type(`${testArchivedMessage}{enter}`);
     
    -        // * Post is not returned by search
    +        // * Post is returned by search since archived channels are always viewable
             cy.get('#searchContainer').should('be.visible');
    -        cy.get('.no-results__wrapper').should('be.visible');
    +        cy.get('.search-item-snippet').first().contains(testArchivedMessage);
         });
     
         it('MM-T1709 Archive a channel while search results are displayed in RHS', () => {
    @@ -123,7 +103,9 @@ describe('archive tests while preventing viewing archived channels', () => {
                 // # While the RHS is still open with the results, archive the channel the post was made in
                 cy.uiArchiveChannel();
                 cy.get('#searchContainer').should('be.visible');
    -            cy.get('.no-results__wrapper').should('be.visible');
    +
    +            // * Search results should remain visible since archived channels are always viewable
    +            cy.get('.search-item-snippet').first().should('contain.text', messageText);
             });
         });
     
    
  • e2e-tests/cypress/tests/integration/channels/archived_channel/archived_channel_spec.ts+0 140 removed
    @@ -1,140 +0,0 @@
    -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
    -// See LICENSE.txt for license information.
    -
    -// ***************************************************************
    -// - [#] indicates a test step (e.g. # Go to a page)
    -// - [*] indicates an assertion (e.g. * Check the title)
    -// - Use element ID when selecting an element. Create one if none.
    -// ***************************************************************
    -
    -// Group: @channels @channel
    -
    -import {getRandomId} from '../../../utils';
    -
    -import {createArchivedChannel} from './helpers';
    -
    -describe('Leave an archived channel', () => {
    -    let testTeam;
    -    let testChannel;
    -    const testArchivedMessage = `this is an archived post ${getRandomId()}`;
    -
    -    before(() => {
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
    -        cy.apiInitSetup().then(({team, channel}) => {
    -            testTeam = team;
    -            testChannel = channel;
    -        });
    -    });
    -
    -    it('MM-T1672_1 User can close archived channel (1/2)', () => {
    -        // # Open a channel that's not the town square
    -        cy.visit(`/${testTeam.name}/channels/off-topic`);
    -
    -        // # repeat searching and navigating to the archived channel steps 3 times.
    -        [1, 2, 3].forEach((i) => {
    -            // * ensure we are not on an archived channel
    -            cy.get('#channelInfoModalLabel span.icon__archive').should('not.exist');
    -
    -            // # Search for a post in an archived channel
    -            cy.uiGetSearchContainer().click();
    -            cy.uiGetSearchBox().clear().type(`${testArchivedMessage}{enter}`);
    -
    -            // # Open the archived channel by selecting Jump from search results and then selecting the link to move to the most recent posts in the channel
    -            cy.uiGetSearchContainer().should('be.visible');
    -
    -            cy.get('a.search-item__jump').first().click();
    -
    -            cy.get(`#sidebarItem_${testChannel.name}`).should('be.visible');
    -
    -            cy.url().should('satisfy', (testUrl) => {
    -                return testUrl.endsWith(`${testTeam.name}/channels/${testChannel.name}`); // wait for permalink to turn into channel url
    -            });
    -
    -            if (i < 3) {
    -                // # Close an archived channel by clicking "Close Channel" button in the footer
    -                cy.get('#channelArchivedMessage button').click();
    -            } else {
    -                // # Click the header menu and select Archive Channel
    -                cy.uiOpenChannelMenu('Archive Channel');
    -            }
    -
    -            // * The user is returned to the channel they were previously viewing and the archived channel is removed from the drawer
    -            cy.get(`#sidebarItem_${testChannel.name}`).should('not.exist');
    -            cy.url().should('include', `${testTeam.name}/channels/off-topic`);
    -        });
    -    });
    -
    -    it('MM-T1672_2 User can close archived channel (2/2)', () => {
    -        // # Add text to channel you land on (after closing the archived channel via Close Channel button)
    -        // * Able to add test
    -        cy.postMessage('some text');
    -        cy.getLastPostId().then((postId) => {
    -            cy.get(`#${postId}_message`).should('be.visible');
    -        });
    -    });
    -
    -    it('MM-T1678 Open an archived channel using CTRL/CMD+K', () => {
    -        // # Select CTRL/CMD+K (or ⌘+K) to open the channel switcher
    -        cy.visit(`/${testTeam.name}/channels/off-topic`);
    -
    -        cy.typeCmdOrCtrl().type('K', {release: true});
    -
    -        // # Start typing the name of a public or private channel on this team that has been archived
    -        cy.findByRole('textbox', {name: 'quick switch input'}).type(testChannel.display_name);
    -
    -        // # Select an archived channel from the list
    -        cy.get('#suggestionList').should('be.visible');
    -        cy.findByTestId(testChannel.name).should('be.visible');
    -        cy.findByTestId(testChannel.name).click();
    -
    -        // * Channel name visible in header
    -        cy.get('#channelHeaderTitle').should('contain', testChannel.display_name);
    -
    -        // * Archived icon is visible in header
    -        cy.get('#channelHeaderInfo .icon-archive-outline').should('be.visible');
    -
    -        // * Channel is listed In drawer
    -        cy.get(`#sidebarItem_${testChannel.name}`).should('be.visible');
    -        cy.get(`#sidebarItem_${testChannel.name} .icon-archive-outline`).should('be.visible');
    -
    -        // * footer shows you are viewing an archived channel
    -        cy.get('#channelArchivedMessage').should('be.visible');
    -    });
    -
    -    it('MM-T1679 Open an archived channel using jump from search results', () => {
    -        // # Create or locate post in an archived channel where the test user had permissions to edit channel details
    -        // generate a sufficiently large set of messages to make the toast appear
    -        const messageList = Array.from({length: 40}, (_, i) => `${i}. any - ${getRandomId()}`);
    -        createArchivedChannel({prefix: 'archived-search-for'}, messageList).then(({name}) => {
    -            // # Locate the post in a search
    -            cy.uiGetSearchContainer().click();
    -            cy.uiGetSearchBox().clear().type(`${messageList[1]}{enter}`);
    -
    -            // # Click jump to open an archive post in permalink view
    -            cy.uiGetSearchContainer().should('be.visible');
    -            cy.get('a.search-item__jump').first().click();
    -
    -            // * Archived channel is opened in permalink view
    -            cy.get('.post--highlight').should('be.visible');
    -
    -            // # Click on You are viewing an archived channel.
    -            cy.get('.toast__jump').should('be.visible').click();
    -
    -            // * Channel is listed In drawer
    -            // * Channel name visible in header
    -            cy.get(`#sidebarItem_${name}`).should('be.visible');
    -            cy.get(`#sidebarItem_${name} .icon-archive-outline`).should('be.visible');
    -
    -            // * Archived icon is visible in header
    -            cy.get('#channelHeaderInfo .icon-archive-outline').should('be.visible');
    -
    -            // * Footer shows "You are viewing an archived channel. New messages cannot be posted. Close Channel"
    -            cy.get('#channelArchivedMessage').should('be.visible');
    -        });
    -    });
    -});
    
  • e2e-tests/cypress/tests/integration/channels/archived_channel/archived_leave_channel_spec.ts+0 187 removed
    @@ -1,187 +0,0 @@
    -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
    -// See LICENSE.txt for license information.
    -
    -// ***************************************************************
    -// - [#] indicates a test step (e.g. # Go to a page)
    -// - [*] indicates an assertion (e.g. * Check the title)
    -// - Use element ID when selecting an element. Create one if none.
    -// ***************************************************************
    -
    -// Group: @channels @channel
    -
    -import * as TIMEOUTS from '../../../fixtures/timeouts';
    -import {getRandomId} from '../../../utils';
    -
    -describe('Leave an archived channel', () => {
    -    let testTeam;
    -    let testUser;
    -    let otherUser;
    -
    -    before(() => {
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
    -        // # Login as test user and visit test channel
    -        cy.apiInitSetup({
    -            loginAfter: true,
    -            promoteNewUserAsAdmin: true,
    -        }).then(({team, user}) => {
    -            testTeam = team;
    -            testUser = user;
    -
    -            cy.apiCreateUser({prefix: 'second'}).then(({user: second}) => {
    -                cy.apiAddUserToTeam(testTeam.id, second.id);
    -                otherUser = second;
    -            });
    -        });
    -    });
    -
    -    it('MM-T1670 Can view channel info for an archived channel', () => {
    -        cy.apiCreateArchivedChannel('archived-a', 'Archived A', 'O', testTeam.id).then((channel) => {
    -            // # Visit the archived channel
    -            cy.visit(`/${testTeam.name}/channels/${channel.name}`);
    -
    -            // # Open channel menu and click View Info
    -            cy.uiOpenChannelMenu('View Info');
    -
    -            // * Channel title is shown with archived icon
    -            cy.get('.sidebar--right__header i.icon-archive-outline').should('be.visible');
    -            cy.contains('.sidebar--right__header', `${channel.display_name}`).should('be.visible');
    -
    -            // * Channel URL is listed (non-linked text)
    -            cy.url().then((loc) => {
    -                cy.contains('div[class^="ChannelLink"]', loc).should('be.visible');
    -            });
    -        });
    -    });
    -
    -    it('MM-T1671 Can view members', () => {
    -        cy.apiCreateChannel(testTeam.id, 'archived-b', 'Archived B').then(({channel}) => {
    -            cy.apiAddUserToChannel(channel.id, otherUser.id);
    -
    -            // # Archive the channel
    -            cy.visit(`/${testTeam.name}/channels/${channel.name}`);
    -            cy.uiArchiveChannel();
    -
    -            // # Open channel menu and click View Members
    -            cy.uiOpenChannelMenu('View members');
    -
    -            // * Channel Members modal opens
    -            cy.get('div#channelMembersModal').should('be.visible');
    -
    -            // # Ensure there are no options to change channel roles or membership
    -            // * Membership or role cannot be changed
    -            cy.findAllByTestId('userListItemActions').eq(0).should('not.be.visible');
    -            cy.findAllByTestId('userListItemActions').eq(1).should('not.be.visible');
    -
    -            // # Use search box to refine list of members
    -            // * Search box works as before to refine member list
    -            cy.get('#searchUsersInput').type(`${otherUser.first_name.substring(0, 5)}{enter}`);
    -            cy.findAllByTestId('userListItemDetails').should('have.length', 2);
    -        });
    -    });
    -
    -    it('MM-T1673 Close channel after viewing two archived channels in a row', () => {
    -        // # Create an archived channel and post a message
    -        const messageC = 'archived channel C message';
    -        cy.apiCreateArchivedChannel('archived-c', 'Archived C', 'O', testTeam.id, [messageC], testUser).then((archivedChannelC) => {
    -            // # Create another archived channel and post a message
    -            const messageD = 'archived channel D message';
    -            cy.apiCreateArchivedChannel('archived-d', 'Archived D', 'O', testTeam.id, [messageD], testUser).then((archivedChannelD) => {
    -                // # Visit town-square and post a message
    -                const previousChannel = `/${testTeam.name}/channels/town-square`;
    -                cy.visit(previousChannel);
    -
    -                // # Search for content from an archived channel
    -                cy.uiGetSearchContainer().click();
    -                cy.uiGetSearchBox().clear().type(`${messageD}{enter}`);
    -
    -                // # Open the channel from search results
    -                cy.get('#searchContainer').should('be.visible');
    -                cy.get('#loadingSpinner').should('not.exist');
    -
    -                // # Open the channel from search result by clicking Jump
    -                cy.get('#searchContainer').should('be.visible').findByText('Jump').click().wait(TIMEOUTS.ONE_SEC);
    -                cy.url().should('contain', `${testTeam.name}/channels/${archivedChannelD.name}`);
    -
    -                // # Search for content from a different archived channel
    -                cy.uiGetSearchContainer().click();
    -                cy.uiGetSearchBox().clear().type(`${messageC}{enter}`);
    -
    -                // # Open the channel from search result by clicking Jump
    -                cy.get('#searchContainer').should('be.visible').findByText('Jump').click().wait(TIMEOUTS.ONE_SEC);
    -                cy.url().should('contain', `${testTeam.name}/channels/${archivedChannelC.name}`);
    -
    -                // # Select "Close Channel"
    -                cy.findByRole('button', {name: 'Close Channel'}).click();
    -
    -                // * User is returned to previously viewed (non-archived) channel
    -                cy.url().should('include', previousChannel);
    -            });
    -        });
    -    });
    -
    -    it('MM-T1674 CTRL/CMD+K list public archived channels you are a member of', () => {
    -        const commonName = `common${getRandomId()}`;
    -        cy.apiCreateChannel(testTeam.id, `${commonName}-a`, commonName).then(({channel}) => {
    -            // # Create a public channel then archived
    -            cy.apiCreateArchivedChannel(`${commonName}-b-archived`, `${commonName} Archived E`, 'O', testTeam.id, ['any message'], testUser).then((archivedChannel) => {
    -                cy.visit(`/${testTeam.name}/channels/off-topic`);
    -
    -                // # Select CTRL/⌘+k) to open the channel switcher
    -                cy.typeCmdOrCtrl().type('K', {release: true});
    -
    -                // # Start typing the name of a public channel on this team that has been archived which the test user belongs to
    -                cy.findByRole('textbox', {name: 'quick switch input'}).type(commonName);
    -
    -                // * Suggestion list should be visible and have three elements (the two channels and the divider)
    -                cy.get('#suggestionList').should('be.visible').children().should('have.length', 3);
    -
    -                // * Both active and archived public channels should be visible
    -                cy.findByTestId(channel.name).should('be.visible').find('.icon-globe').should('be.visible');
    -                cy.findByTestId(archivedChannel.name).should('be.visible').find('.icon-archive-outline').should('be.visible');
    -            });
    -        });
    -    });
    -
    -    it('MM-T1675 CTRL/CMD+K list private archived channels you are a member of', () => {
    -        const commonName = `common${getRandomId()}`;
    -        cy.apiCreateChannel(testTeam.id, `${commonName}-a`, commonName).then(({channel}) => {
    -            // # Create a private channel then archived
    -            cy.apiCreateArchivedChannel(`${commonName}-b-archived`, `${commonName} Archived F`, 'P', testTeam.id, ['any message'], testUser).then((archivedChannel) => {
    -                cy.visit(`/${testTeam.name}/channels/off-topic`);
    -
    -                // # Select CTRL/⌘+k) to open the channel switcher
    -                cy.typeCmdOrCtrl().type('K', {release: true});
    -
    -                // # Start typing the name of a private channel on this team that has been archived which the test user belongs to
    -                cy.findByRole('textbox', {name: 'quick switch input'}).type(commonName);
    -
    -                // * Suggestion list should be visible and have three elements (the two channels and the divider)
    -                cy.get('#suggestionList').should('be.visible').children().should('have.length', 3);
    -
    -                // * Both active public and archived private channels should be visible
    -                cy.findByTestId(channel.name).should('be.visible').find('.icon-globe').should('be.visible');
    -                cy.findByTestId(archivedChannel.name).should('be.visible').find('.icon-archive-outline').should('be.visible');
    -            });
    -        });
    -    });
    -
    -    it('MM-T1676 CTRL/CMD+K does not show private archived channels you are not a member of', () => {
    -        // # As another user, create or locate a private channel that the test user is not a member of and archive the channel
    -        cy.apiLogin(otherUser);
    -        cy.apiCreateArchivedChannel('archived-g', 'Archived G', 'O', testTeam.id, ['any message'], testUser).then((archivedChannel) => {
    -            // # As the test user, select CTRL/CMD+K (or ⌘+k) to open the channel switcher
    -            cy.apiLogin(testUser);
    -            cy.visit(`/${testTeam.name}/channels/off-topic`);
    -            cy.contains('#channelHeaderTitle', 'Off-Topic');
    -            cy.typeCmdOrCtrl().type('K', {release: true});
    -
    -            // * Private archived channels you are not a member of are not available on channel switcher
    -            cy.contains('#suggestionList', archivedChannel.name).should('not.exist');
    -        });
    -    });
    -});
    
  • e2e-tests/cypress/tests/integration/channels/archived_channel/join_archived_channel_spec.ts+0 6 modified
    @@ -18,12 +18,6 @@ describe('Archived channels', () => {
         let testUser;
     
         before(() => {
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             cy.apiInitSetup().then(({team, user}) => {
                 testTeam = team;
                 testUser = user;
    
  • e2e-tests/cypress/tests/integration/channels/archived_channel/leave_archived_channel_spec.ts+0 6 modified
    @@ -14,12 +14,6 @@ describe('Leaving archived channels', () => {
         let testTeam;
     
         before(() => {
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             cy.apiInitSetup({loginAfter: true}).then(({team}) => {
                 testTeam = team;
             });
    
  • e2e-tests/cypress/tests/integration/channels/archived_channel/post_menu_spec.ts+0 74 removed
    @@ -1,74 +0,0 @@
    -// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
    -// See LICENSE.txt for license information.
    -
    -// ***************************************************************
    -// - [#] indicates a test step (e.g. # Go to a page)
    -// - [*] indicates an assertion (e.g. * Check the title)
    -// - Use element ID when selecting an element. Create one if none.
    -// ***************************************************************
    -
    -// Group: @channels @channel
    -
    -describe('Archived channels', () => {
    -    before(() => {
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
    -        // # Login as test user and visit created channel
    -        cy.apiInitSetup({loginAfter: true}).then(({team, channel}) => {
    -            cy.visit(`/${team.name}/channels/${channel.name}`);
    -        });
    -    });
    -
    -    it('MM-T1721 Archive channel posts menu should have copy link and reply options', () => {
    -        // # Click to add a channel description
    -        //   Note that it is invisible until the mouse hovers it, which is unfeasible in Cypress
    -        //   https://docs.cypress.io/api/commands/hover
    -        cy.findByText('Add a channel header').click({force: true});
    -
    -        // # Add channel header for system message
    -        const header = 'this is a header!';
    -        cy.get('#editChannelHeaderModalLabel').should('be.visible');
    -        cy.get('textarea#edit_textbox').should('be.visible').type(`${header}{enter}`);
    -
    -        cy.getLastPostId().then((postId) => {
    -            // * Check post if it is the system message
    -            cy.get(`#postMessageText_${postId}`).should('contain', header);
    -            cy.clickPostDotMenu(postId).then(() => {
    -                // * Copy link menu item should not be visible
    -                cy.findByText('Copy Link').should('not.exist');
    -
    -                // * Reply post menu item should not be visible
    -                cy.findByText('Reply').should('not.exist');
    -                cy.get(`#CENTER_dropdown_${postId}`).should('be.visible').type('{esc}');
    -            });
    -        });
    -
    -        const messageText = 'Test archive channel post menu';
    -
    -        // # Post a message in the channel
    -        cy.postMessage(messageText);
    -
    -        // # Get the last post for reference of ID
    -        cy.getLastPostId().then((postId) => {
    -            // # Archive the channel
    -            cy.uiArchiveChannel();
    -
    -            // # Wait until a system message is posted that the channel has been archived
    -            cy.uiWaitUntilMessagePostedIncludes('archived the channel');
    -
    -            // # Click on post dot menu
    -            cy.clickPostDotMenu(postId);
    -
    -            // * Copy link menu item should be visible
    -            cy.findByText('Copy Link').scrollIntoView().should('be.visible');
    -
    -            // * Reply post menu item should be visible
    -            cy.findByText('Reply').should('be.visible');
    -            cy.get(`#CENTER_dropdown_${postId}`).should('be.visible').type('{esc}');
    -        });
    -    });
    -});
    
  • e2e-tests/cypress/tests/integration/channels/channel/archived_channels_1_spec.ts+0 33 modified
    @@ -23,12 +23,6 @@ describe('Leave an archived channel', () => {
             archived: 'Channel Type: Archived',
         };
         before(() => {
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             // # Login as test user and visit off-topic
             cy.apiInitSetup({loginAfter: true}).then(({team, offTopicUrl: url}) => {
                 testTeam = team;
    @@ -286,31 +280,4 @@ describe('Leave an archived channel', () => {
             });
             cy.get('body').typeWithForce('{esc}');
         });
    -
    -    it('MM-T1696 - When clicking Browse Channels no options for archived channels are shown when the feature is disabled', () => {
    -        cy.apiAdminLogin();
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: false,
    -            },
    -        });
    -
    -        // # Create public channel
    -        cy.apiCreateChannel(testTeam.id, 'channel', 'channel').then(({channel}) => {
    -            // # Visit the channel
    -            cy.visit(`/${testTeam.name}/channels/${channel.name}`);
    -
    -            // # Archive the channel
    -            cy.uiArchiveChannel();
    -
    -            // # Click on browse channels from menu
    -            cy.uiBrowseOrCreateChannel('Browse channels');
    -
    -            // # Modal should not contain the created channel
    -            cy.findByText(channelType.all).should('be.visible').click();
    -            cy.findByText('Archived channels').should('not.exist');
    -            cy.get('#moreChannelsList').should('not.contain', channel.name);
    -        });
    -        cy.get('body').typeWithForce('{esc}');
    -    });
     });
    
  • e2e-tests/cypress/tests/integration/channels/channel/archived_channels_2_spec.ts+0 6 modified
    @@ -21,12 +21,6 @@ describe('Leave an archived channel', () => {
         let testUser: UserProfile;
     
         before(() => {
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             cy.apiInitSetup().then(({team, user}) => {
                 testTeam = team;
                 testUser = user;
    
  • e2e-tests/cypress/tests/integration/channels/channel/browse_channels_spec.ts+18 47 modified
    @@ -52,19 +52,28 @@ describe('Channels', () => {
             });
         });
     
    -    it('MM-19337 Verify UI of Browse channels modal with archived selection', () => {
    -        verifyBrowseChannelsModalWithArchivedSelection(false, testUser, testTeam);
    -        verifyBrowseChannelsModalWithArchivedSelection(true, testUser, testTeam);
    +    it('MM-19337 Verify UI of Browse channels modal with archived selection as admin', () => {
    +        // * Verify browse channels modal as admin shows archived channels option
    +        cy.visit(`/${testTeam.name}/channels/town-square`);
    +        cy.uiBrowseOrCreateChannel('Browse channels');
    +        cy.get('#browseChannelsModal').should('be.visible').within(() => {
    +            cy.get('#menuWrapper').should('be.visible').and('have.text', channelType.all);
    +        });
    +        cy.get('body').typeWithForce('{esc}');
         });
     
    -    it('MM-19337 Enable users to view archived channels', () => {
    -        cy.apiAdminLogin();
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    +    it('MM-19337 Verify UI of Browse channels modal with archived selection as regular user', () => {
    +        // # Login as regular user and verify browse channels modal shows archived channels option
    +        cy.apiLogin(testUser);
    +        cy.visit(`/${testTeam.name}/channels/town-square`);
    +        cy.uiBrowseOrCreateChannel('Browse channels');
    +        cy.get('#browseChannelsModal').should('be.visible').within(() => {
    +            cy.get('#menuWrapper').should('be.visible').and('have.text', channelType.all);
             });
    +        cy.get('body').typeWithForce('{esc}');
    +    });
     
    +    it('MM-19337 Enable users to view archived channels', () => {
             // # Login as new user and go to "/"
             cy.apiLogin(otherUser);
             cy.visit(`/${testTeam.name}/channels/town-square`);
    @@ -183,11 +192,6 @@ describe('Channels', () => {
     
         it('MM-T1702 Search works when changing public/all options in the dropdown', () => {
             cy.apiAdminLogin();
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
             let newChannel: Channel;
             let testArchivedChannel: Channel;
             let testPrivateArchivedChannel: Channel;
    @@ -267,36 +271,3 @@ describe('Channels', () => {
         });
     });
     
    -function verifyBrowseChannelsModalWithArchivedSelection(isEnabled, testUser, testTeam) {
    -    // # Login as sysadmin and Update config to enable/disable viewing of archived channels
    -    cy.apiAdminLogin();
    -    cy.apiUpdateConfig({
    -        TeamSettings: {
    -            ExperimentalViewArchivedChannels: isEnabled,
    -        },
    -    });
    -
    -    // * Verify browse channels modal
    -    cy.visit(`/${testTeam.name}/channels/town-square`);
    -    verifyBrowseChannelsModal(isEnabled);
    -
    -    // # Login as regular user and verify browse channels modal
    -    cy.apiLogin(testUser);
    -    cy.visit(`/${testTeam.name}/channels/town-square`);
    -    verifyBrowseChannelsModal(isEnabled);
    -}
    -
    -function verifyBrowseChannelsModal(isEnabled) {
    -    // # Go to LHS and click 'Browse channels'
    -    cy.uiBrowseOrCreateChannel('Browse channels');
    -
    -    // * Verify that the browse channels modal is open and with or without option to view archived channels
    -    cy.get('#browseChannelsModal').should('be.visible').within(() => {
    -        if (isEnabled) {
    -            cy.get('#menuWrapper').should('be.visible').and('have.text', channelType.all);
    -        } else {
    -            cy.get('#menuWrapper').click();
    -            cy.findByText('Archived channels').should('not.exist');
    -        }
    -    });
    -}
    
  • e2e-tests/cypress/tests/integration/channels/channel/channel_settings_modal_spec.ts+27 23 modified
    @@ -298,38 +298,42 @@ describe('Channel Settings Modal', () => {
             cy.get('.SaveChangesPanel').should('contain', 'There are errors in the form above');
         });
     
    -    it('MM-T9: Can archive a channel and redirect to previous visited channel', () => {
    +    it('MM-T9: Can archive a channel and stay in archived channel view', () => {
             // # Create a new channel for this test
    -        cy.apiCreateChannel(testTeam.id, 'first-channel', 'First Channel').then(({channel: channel1}) => {
    -            cy.apiCreateChannel(testTeam.id, 'second-channel', 'Second Channel').then(({channel}) => {
    -            // # visit town square
    -                cy.visit(`/${testTeam.name}/channels/${channel1.name}`);
    +        cy.apiCreateChannel(testTeam.id, 'archived-channel-test', 'Archived Channel Test').then(({channel}) => {
    +            // # Visit the channel to be archived
    +            cy.visit(`/${testTeam.name}/channels/${channel.name}`);
     
    -                // # visit just created channel
    -                cy.visit(`/${testTeam.name}/channels/${channel.name}`);
    +            // # Open channel settings modal
    +            cy.get('#channelHeaderDropdownButton').click();
    +            cy.findByText('Channel Settings').click();
     
    -                // # Open channel settings modal
    -                cy.get('#channelHeaderDropdownButton').click();
    -                cy.findByText('Channel Settings').click();
    +            // # Click on Archive tab
    +            cy.get('#archiveButton').click();
     
    -                // # Click on Archive tab
    -                cy.get('#archiveButton').click();
    +            // # Click Archive button
    +            cy.get('#channelSettingsArchiveChannelButton').click();
     
    -                // # Click Archive button
    -                cy.get('#channelSettingsArchiveChannelButton').click();
    +            // * Verify confirmation modal appears
    +            cy.get('#archiveChannelConfirmModal').should('be.visible');
     
    -                // * Verify confirmation modal appears
    -                cy.get('#archiveChannelConfirmModal').should('be.visible');
    +            // # Confirm archive
    +            cy.findByRole('button', {name: 'Confirm'}).click();
     
    -                // # Confirm archive
    -                cy.findByRole('button', {name: 'Confirm'}).click();
    +            // * Verify we stay in the archived channel (no redirect)
    +            cy.url().should('include', channel.name);
     
    -                // * Verify redirect to Town Square
    -                cy.url().should('include', channel1.name);
    +            // * Verify archived channel message is displayed
    +            cy.contains('You are viewing an archived channel').should('be.visible');
     
    -                // * Verify channel is no longer in sidebar
    -                cy.get('.SidebarChannel').contains('Archive Test').should('not.exist');
    -            });
    +            // * Verify channel is still visible in sidebar while viewing it
    +            cy.get('.SidebarChannel').contains('Archived Channel Test').should('exist');
    +
    +            // # Navigate to a different channel
    +            cy.visit(`/${testTeam.name}/channels/town-square`);
    +
    +            // * Verify archived channel disappears from sidebar after navigating away
    +            cy.get('.SidebarChannel').contains('Archived Channel Test').should('not.exist');
             });
         });
     
    
  • e2e-tests/cypress/tests/integration/channels/channel_sidebar/channel_sidebar_spec.ts+13 4 modified
    @@ -103,7 +103,7 @@ describe('Channel sidebar', () => {
             cy.get('.SidebarChannel:contains(Off-Topic)').should('not.exist');
         });
     
    -    it('MM-T1684 should remove channel from sidebar after deleting it', () => {
    +    it('MM-T1684 should remove channel from sidebar after deleting it and navigate away', () => {
             // # Start with a new team
             const teamName = `team-${getRandomId()}`;
             cy.createNewTeam(teamName, teamName);
    @@ -122,10 +122,19 @@ describe('Channel sidebar', () => {
             cy.get('#channelArchiveChannel').should('be.visible').click();
             cy.get('#deleteChannelModalDeleteButton').should('be.visible').click();
     
    -        // * Verify that we've switched to Town Square
    -        verifyChannelSwitch('Town Square', `/${teamName}/channels/town-square`);
    +        // * Verify we stay in the archived channel (no redirect)
    +        cy.url().should('include', `/${teamName}/channels/off-topic`);
     
    -        // * Verify that Off Topic has disappeared from the sidebar
    +        // * Verify archived channel message is displayed
    +        cy.contains('You are viewing an archived channel').should('be.visible');
    +
    +        // * Verify channel is still visible in sidebar while viewing it
    +        cy.get('.SidebarChannel:contains(Off-Topic)').should('exist');
    +
    +        // # Navigate to a different channel
    +        cy.visit(`/${teamName}/channels/town-square`);
    +
    +        // * Verify archived channel disappears from sidebar after navigating away
             cy.get('.SidebarChannel:contains(Off-Topic)').should('not.exist');
         });
     
    
  • e2e-tests/cypress/tests/integration/channels/enterprise/system_console/archived_channels_spec.js+0 6 modified
    @@ -18,12 +18,6 @@ describe('Archived channels', () => {
         before(() => {
             cy.apiRequireLicense();
     
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             cy.apiInitSetup({
                 channelPrefix: {name: '000-archive', displayName: '000 Archive Test'},
             }).then(({channel}) => {
    
  • e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/not_open_emoji_picker_spec.js+0 7 modified
    @@ -23,13 +23,6 @@ describe('Keyboard shortcut CTRL/CMD+Shift+\\ for adding reaction to last messag
         let emptyChannel;
     
         before(() => {
    -        // # Enable Experimental View Archived Channels
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             cy.apiInitSetup().then(({team, channel, user}) => {
                 testUser = user;
                 testTeam = team;
    
  • e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/react_to_center_spec.js+0 7 modified
    @@ -25,13 +25,6 @@ describe('Keyboard shortcut CTRL/CMD+Shift+\\ for adding reaction to last messag
         let offTopicChannel;
     
         before(() => {
    -        // # Enable Experimental View Archived Channels
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             cy.apiInitSetup().then(({team, user}) => {
                 testUser = user;
                 testTeam = team;
    
  • e2e-tests/cypress/tests/integration/channels/keyboard_shortcuts/ctrl_cmd_shift_slash/react_to_rhs_spec.js+0 7 modified
    @@ -23,13 +23,6 @@ describe('Keyboard shortcut CTRL/CMD+Shift+\\ for adding reaction to last messag
         let testTeam;
     
         before(() => {
    -        // # Enable Experimental View Archived Channels
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             cy.apiInitSetup().then(({team, user}) => {
                 testUser = user;
                 testTeam = team;
    
  • e2e-tests/cypress/tests/integration/channels/mark_as_unread/archive_channel_mark_as_unread_spec.js+0 7 modified
    @@ -19,13 +19,6 @@ describe('Channels', () => {
         let post1;
     
         before(() => {
    -        // # Enable Experimental View Archived Channels
    -        cy.apiUpdateConfig({
    -            TeamSettings: {
    -                ExperimentalViewArchivedChannels: true,
    -            },
    -        });
    -
             cy.apiInitSetup().then(({team, user}) => {
                 testUser = user;
                 testTeam = team;
    
  • e2e-tests/cypress/tests/support/api/cloud_default_config.json+0 1 modified
    @@ -128,7 +128,6 @@
             "MaxNotificationsPerChannel": 1000,
             "EnableConfirmNotificationsToChannel": true,
             "TeammateNameDisplay": "username",
    -        "ExperimentalViewArchivedChannels": false,
             "ExperimentalEnableAutomaticReplies": false,
             "LockTeammateNameDisplay": false,
             "ExperimentalPrimaryTeam": "",
    
  • e2e-tests/cypress/tests/support/api/on_prem_default_config.json+0 1 modified
    @@ -130,7 +130,6 @@
             "MaxNotificationsPerChannel": 1000,
             "EnableConfirmNotificationsToChannel": true,
             "TeammateNameDisplay": "username",
    -        "ExperimentalViewArchivedChannels": false,
             "ExperimentalEnableAutomaticReplies": false,
             "LockTeammateNameDisplay": false,
             "ExperimentalPrimaryTeam": "",
    
  • e2e-tests/playwright/lib/src/server/default_config.ts+0 1 modified
    @@ -223,7 +223,6 @@ const defaultServerConfig: AdminConfig = {
             MaxNotificationsPerChannel: 1000,
             EnableConfirmNotificationsToChannel: true,
             TeammateNameDisplay: 'username',
    -        ExperimentalViewArchivedChannels: true,
             ExperimentalEnableAutomaticReplies: false,
             LockTeammateNameDisplay: false,
             ExperimentalPrimaryTeam: '',
    
  • server/channels/api4/channel_bookmark.go+0 7 modified
    @@ -416,13 +416,6 @@ func listChannelBookmarksForChannel(c *Context, w http.ResponseWriter, r *http.R
     		return
     	}
     
    -	if !*c.App.Config().TeamSettings.ExperimentalViewArchivedChannels {
    -		if channel.DeleteAt != 0 {
    -			c.Err = model.NewAppError("listChannelBookmarksForChannel", "api.user.view_archived_channels.list_channel_bookmarks_for_channel.app_error", nil, "", http.StatusForbidden)
    -			return
    -		}
    -	}
    -
     	if !c.App.SessionHasPermissionToReadChannel(c.AppContext, *c.AppContext.Session(), channel) {
     		c.SetPermissionError(model.PermissionReadChannelContent)
     		return
    
  • server/channels/api4/channel_bookmark_test.go+1 46 modified
    @@ -1651,52 +1651,7 @@ func TestListChannelBookmarksForChannel(t *testing.T) {
     		}
     	})
     
    -	t.Run("bookmark listing should not work in an archived channel without ExperimentalViewArchivedChannels", func(t *testing.T) {
    -		experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
    -		defer func() {
    -			th.App.UpdateConfig(func(cfg *model.Config) {
    -				cfg.TeamSettings.ExperimentalViewArchivedChannels = &experimentalViewArchivedChannels
    -			})
    -		}()
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = false
    -		})
    -
    -		channelBookmark := &model.ChannelBookmark{
    -			ChannelId:   th.BasicDeletedChannel.Id,
    -			DisplayName: "Link bookmark test",
    -			LinkUrl:     "https://mattermost.com",
    -			Type:        model.ChannelBookmarkLink,
    -			Emoji:       ":smile:",
    -		}
    -
    -		_, _, _ = th.SystemAdminClient.RestoreChannel(context.Background(), channelBookmark.ChannelId)
    -
    -		cb, resp, err := th.Client.CreateChannelBookmark(context.Background(), channelBookmark)
    -		require.NoError(t, err)
    -		CheckCreatedStatus(t, resp)
    -		require.NotNil(t, cb)
    -
    -		_, _ = th.SystemAdminClient.DeleteChannel(context.Background(), cb.ChannelId)
    -
    -		// try to list the channel bookmarks
    -		bookmarks, resp, err := th.Client.ListChannelBookmarksForChannel(context.Background(), cb.ChannelId, 0)
    -		require.Error(t, err)
    -		CheckForbiddenStatus(t, resp)
    -		require.Nil(t, bookmarks)
    -	})
    -
    -	t.Run("bookmark listing should work in an archived channel with ExperimentalViewArchivedChannels", func(t *testing.T) {
    -		experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
    -		defer func() {
    -			th.App.UpdateConfig(func(cfg *model.Config) {
    -				cfg.TeamSettings.ExperimentalViewArchivedChannels = &experimentalViewArchivedChannels
    -			})
    -		}()
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = true
    -		})
    -
    +	t.Run("bookmark listing should work in an archived channel", func(t *testing.T) {
     		channelBookmark := &model.ChannelBookmark{
     			ChannelId:   th.BasicDeletedChannel.Id,
     			DisplayName: "Link bookmark test",
    
  • server/channels/api4/file_test.go+2 23 modified
    @@ -1365,15 +1365,6 @@ func TestSearchFilesInTeam(t *testing.T) {
     	mainHelper.Parallel(t)
     	th := Setup(t).InitBasic()
     	defer th.TearDown()
    -	experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
    -	defer func() {
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			cfg.TeamSettings.ExperimentalViewArchivedChannels = &experimentalViewArchivedChannels
    -		})
    -	}()
    -	th.App.UpdateConfig(func(cfg *model.Config) {
    -		*cfg.TeamSettings.ExperimentalViewArchivedChannels = true
    -	})
     	data, err := testutils.ReadTestFile("test.png")
     	require.NoError(t, err)
     
    @@ -1478,13 +1469,10 @@ func TestSearchFilesInTeam(t *testing.T) {
     	require.NoError(t, err)
     	require.Len(t, fileInfos.Order, 3, "wrong search")
     
    -	th.App.UpdateConfig(func(cfg *model.Config) {
    -		*cfg.TeamSettings.ExperimentalViewArchivedChannels = false
    -	})
    -
    +	// Archived channels are always included now, so this should return the same result
     	fileInfos, _, err = client.SearchFilesWithParams(context.Background(), th.BasicTeam.Id, &searchParams)
     	require.NoError(t, err)
    -	require.Len(t, fileInfos.Order, 2, "wrong search")
    +	require.Len(t, fileInfos.Order, 3, "wrong search")
     
     	fileInfos, _, _ = client.SearchFiles(context.Background(), th.BasicTeam.Id, "*", false)
     	require.Empty(t, fileInfos.Order, "searching for just * shouldn't return any results")
    @@ -1516,15 +1504,6 @@ func TestSearchFilesAcrossTeams(t *testing.T) {
     	mainHelper.Parallel(t)
     	th := Setup(t).InitBasic()
     	defer th.TearDown()
    -	experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
    -	defer func() {
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			cfg.TeamSettings.ExperimentalViewArchivedChannels = &experimentalViewArchivedChannels
    -		})
    -	}()
    -	th.App.UpdateConfig(func(cfg *model.Config) {
    -		*cfg.TeamSettings.ExperimentalViewArchivedChannels = true
    -	})
     	data, err := testutils.ReadTestFile("test.png")
     	require.NoError(t, err)
     
    
  • server/channels/api4/post.go+0 12 modified
    @@ -222,18 +222,6 @@ func getPostsForChannel(c *Context, w http.ResponseWriter, r *http.Request) {
     		return
     	}
     
    -	if !*c.App.Config().TeamSettings.ExperimentalViewArchivedChannels {
    -		channel, appErr := c.App.GetChannel(c.AppContext, channelId)
    -		if appErr != nil {
    -			c.Err = appErr
    -			return
    -		}
    -		if channel.DeleteAt != 0 {
    -			c.Err = model.NewAppError("Api4.getPostsForChannel", "api.user.view_archived_channels.get_posts_for_channel.app_error", nil, "", http.StatusForbidden)
    -			return
    -		}
    -	}
    -
     	var list *model.PostList
     	etag := ""
     
    
  • server/channels/api4/post_test.go+3 28 modified
    @@ -2350,22 +2350,9 @@ func TestGetPostsForChannel(t *testing.T) {
     		_, err = th.SystemAdminClient.DeleteChannel(context.Background(), channel.Id)
     		require.NoError(t, err)
     
    -		experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
    -		th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalViewArchivedChannels = true })
    -		defer th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = experimentalViewArchivedChannels
    -		})
    -
    -		// the endpoint should work fine when viewing archived channels is enabled
     		_, _, err = c.GetPostsForChannel(context.Background(), channel.Id, 0, 10, "", false, false)
     		require.NoError(t, err)
    -
    -		// the endpoint should return forbidden if viewing archived channels is disabled
    -		th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalViewArchivedChannels = false })
    -		_, resp, err = c.GetPostsForChannel(context.Background(), channel.Id, 0, 10, "", false, false)
    -		require.Error(t, err)
    -		CheckForbiddenStatus(t, resp)
    -	}, "Should forbid to retrieve posts if the channel is archived and users are not allowed to view archived messages")
    +	}, "Should allow retrieving posts if the channel is archived")
     
     	_, err = client.DeletePost(context.Background(), post10.Id)
     	require.NoError(t, err)
    @@ -3838,15 +3825,6 @@ func TestSearchPosts(t *testing.T) {
     
     	th := Setup(t).InitBasic()
     	defer th.TearDown()
    -	experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
    -	defer func() {
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			cfg.TeamSettings.ExperimentalViewArchivedChannels = &experimentalViewArchivedChannels
    -		})
    -	}()
    -	th.App.UpdateConfig(func(cfg *model.Config) {
    -		*cfg.TeamSettings.ExperimentalViewArchivedChannels = true
    -	})
     
     	th.LoginBasic()
     	client := th.Client
    @@ -3953,13 +3931,10 @@ func TestSearchPosts(t *testing.T) {
     	require.NoError(t, err)
     	require.Len(t, posts.Order, 2, "wrong search")
     
    -	th.App.UpdateConfig(func(cfg *model.Config) {
    -		*cfg.TeamSettings.ExperimentalViewArchivedChannels = false
    -	})
    -
    +	// Archived channels are always included now, so this should return the same result
     	posts, _, err = client.SearchPostsWithParams(context.Background(), th.BasicTeam.Id, &searchParams)
     	require.NoError(t, err)
    -	require.Len(t, posts.Order, 1, "wrong search")
    +	require.Len(t, posts.Order, 2, "wrong search")
     
     	posts, _, _ = client.SearchPosts(context.Background(), th.BasicTeam.Id, "*", false)
     	require.Empty(t, posts.Order, "searching for just * shouldn't return any results")
    
  • server/channels/api4/user.go+0 14 modified
    @@ -811,20 +811,6 @@ func getUsers(c *Context, w http.ResponseWriter, r *http.Request) {
     		etag     string
     	)
     
    -	if inChannelId != "" {
    -		if !*c.App.Config().TeamSettings.ExperimentalViewArchivedChannels {
    -			channel, cErr := c.App.GetChannel(c.AppContext, inChannelId)
    -			if cErr != nil {
    -				c.Err = cErr
    -				return
    -			}
    -			if channel.DeleteAt != 0 {
    -				c.Err = model.NewAppError("Api4.getUsersInChannel", "api.user.view_archived_channels.get_users_in_channel.app_error", nil, "", http.StatusForbidden)
    -				return
    -			}
    -		}
    -	}
    -
     	if withoutTeamBool, _ := strconv.ParseBool(withoutTeam); withoutTeamBool {
     		// Use a special permission for now
     		if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionListUsersWithoutTeam) {
    
  • server/channels/api4/user_test.go+2 24 modified
    @@ -3320,7 +3320,7 @@ func TestGetUsersInChannel(t *testing.T) {
     	_, _, err = th.SystemAdminClient.GetUsersInChannel(context.Background(), channelId, 0, 60, "")
     	require.NoError(t, err)
     
    -	t.Run("Should forbid getting the members of an archived channel if users are not allowed to view archived messages", func(t *testing.T) {
    +	t.Run("Should allow getting the members of an archived channel", func(t *testing.T) {
     		th.LoginBasic()
     		channel, _, appErr := th.SystemAdminClient.CreateChannel(context.Background(), &model.Channel{
     			DisplayName: "User Created Channel",
    @@ -3336,34 +3336,11 @@ func TestGetUsersInChannel(t *testing.T) {
     		_, err = th.SystemAdminClient.DeleteChannel(context.Background(), channel.Id)
     		require.NoError(t, err)
     
    -		experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
    -		th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalViewArchivedChannels = true })
    -		defer th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = experimentalViewArchivedChannels
    -		})
    -
    -		// the endpoint should work fine for all clients when viewing
    -		// archived channels is enabled
     		for _, client := range []*model.Client4{th.SystemAdminClient, th.Client, th.LocalClient} {
     			users, _, userErr := client.GetUsersInChannel(context.Background(), channel.Id, 0, 1000, "")
     			require.NoError(t, userErr)
     			require.Len(t, users, 3)
     		}
    -
    -		// the endpoint should return forbidden if viewing archived
    -		// channels is disabled for all clients but the Local one
    -		th.App.UpdateConfig(func(cfg *model.Config) { *cfg.TeamSettings.ExperimentalViewArchivedChannels = false })
    -		for _, client := range []*model.Client4{th.SystemAdminClient, th.Client} {
    -			users, resp, userErr := client.GetUsersInChannel(context.Background(), channel.Id, 0, 1000, "")
    -			require.Error(t, userErr)
    -			require.Len(t, users, 0)
    -			CheckForbiddenStatus(t, resp)
    -		}
    -
    -		// local client should be able to get the users still
    -		users, _, appErr := th.LocalClient.GetUsersInChannel(context.Background(), channel.Id, 0, 1000, "")
    -		require.NoError(t, appErr)
    -		require.Len(t, users, 3)
     	})
     }
     
    @@ -9393,6 +9370,7 @@ func TestResetPasswordFailedAttempts(t *testing.T) {
     		require.Equal(t, int(0), sysadminUser.FailedAttempts)
     	})
     }
    +
     func TestSearchUsersWithMfaEnforced(t *testing.T) {
     	th := Setup(t).InitBasic()
     	defer th.TearDown()
    
  • server/channels/app/authorization.go+8 28 modified
    @@ -100,16 +100,13 @@ func (a *App) SessionHasPermissionToChannel(c request.CTX, session model.Session
     		return false
     	} else if appErr != nil {
     		c.Logger().Warn("Failed to get channel", mlog.String("channel_id", channelID), mlog.Err(appErr))
    +		return false
     	}
     
     	if session.IsUnrestricted() || a.RolesGrantPermission(session.GetUserRoles(), model.PermissionManageSystem.Id) {
     		return true
     	}
     
    -	if appErr == nil && a.isChannelArchivedAndHidden(channel) {
    -		return false
    -	}
    -
     	ids, err := a.Srv().Store().Channel().GetAllChannelMembersForUser(c, session.UserId, true, true)
     	var channelRoles []string
     	if err == nil {
    @@ -121,7 +118,7 @@ func (a *App) SessionHasPermissionToChannel(c request.CTX, session model.Session
     		}
     	}
     
    -	if appErr == nil && channel.TeamId != "" {
    +	if channel.TeamId != "" {
     		return a.SessionHasPermissionToTeam(session, channel.TeamId, permission)
     	}
     
    @@ -138,22 +135,15 @@ func (a *App) SessionHasPermissionToChannels(c request.CTX, session model.Sessio
     		return true
     	}
     
    +	// make sure all channels exist, otherwise return false.
     	for _, channelID := range channelIDs {
     		if channelID == "" {
     			return false
     		}
     
    -		// make sure all channels exist, otherwise return false.
    -		for _, channelID := range channelIDs {
    -			channel, appErr := a.GetChannel(c, channelID)
    -			if appErr != nil {
    -				return false
    -			}
    -
    -			// if any channel is archived and the user doesn't have permission to view archived channels, return false
    -			if a.isChannelArchivedAndHidden(channel) {
    -				return false
    -			}
    +		_, appErr := a.GetChannel(c, channelID)
    +		if appErr != nil {
    +			return false
     		}
     	}
     
    @@ -205,7 +195,7 @@ func (a *App) SessionHasPermissionToChannelByPost(session model.Session, postID
     		return false
     	}
     
    -	if channelMember, err := a.Srv().Store().Channel().GetMemberForPost(postID, session.UserId, *a.Config().TeamSettings.ExperimentalViewArchivedChannels); err == nil {
    +	if channelMember, err := a.Srv().Store().Channel().GetMemberForPost(postID, session.UserId); err == nil {
     		if a.RolesGrantPermission(channelMember.GetRoles(), permission.Id) {
     			return true
     		}
    @@ -325,7 +315,7 @@ func (a *App) HasPermissionToChannel(c request.CTX, askingUserId string, channel
     }
     
     func (a *App) HasPermissionToChannelByPost(c request.CTX, askingUserId string, postID string, permission *model.Permission) bool {
    -	if channelMember, err := a.Srv().Store().Channel().GetMemberForPost(postID, askingUserId, *a.Config().TeamSettings.ExperimentalViewArchivedChannels); err == nil {
    +	if channelMember, err := a.Srv().Store().Channel().GetMemberForPost(postID, askingUserId); err == nil {
     		if a.RolesGrantPermission(channelMember.GetRoles(), permission.Id) {
     			return true
     		}
    @@ -417,9 +407,6 @@ func (a *App) SessionHasPermissionToReadChannel(c request.CTX, session model.Ses
     }
     
     func (a *App) HasPermissionToReadChannel(c request.CTX, userID string, channel *model.Channel) bool {
    -	if a.isChannelArchivedAndHidden(channel) {
    -		return false
    -	}
     	if a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionReadChannelContent) {
     		return true
     	}
    @@ -432,9 +419,6 @@ func (a *App) HasPermissionToReadChannel(c request.CTX, userID string, channel *
     }
     
     func (a *App) HasPermissionToChannelMemberCount(c request.CTX, userID string, channel *model.Channel) bool {
    -	if a.isChannelArchivedAndHidden(channel) {
    -		return false
    -	}
     	if a.HasPermissionToChannel(c, userID, channel.Id, model.PermissionReadChannelContent) {
     		return true
     	}
    @@ -445,7 +429,3 @@ func (a *App) HasPermissionToChannelMemberCount(c request.CTX, userID string, ch
     
     	return false
     }
    -
    -func (a *App) isChannelArchivedAndHidden(channel *model.Channel) bool {
    -	return !*a.Config().TeamSettings.ExperimentalViewArchivedChannels && channel.DeleteAt != 0
    -}
    
  • server/channels/app/authorization_test.go+7 109 modified
    @@ -241,19 +241,7 @@ func TestSessionHasPermissionToChannel(t *testing.T) {
     		assert.True(t, th.App.SessionHasPermissionToChannel(th.Context, session, th.BasicChannel.Id, model.PermissionAddReaction))
     	})
     
    -	t.Run("basic user cannot access archived channel if setting is off", func(t *testing.T) {
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = false
    -		})
    -		err := th.App.DeleteChannel(th.Context, th.BasicChannel, th.SystemAdminUser.Id)
    -		require.Nil(t, err)
    -		assert.False(t, th.App.SessionHasPermissionToChannel(th.Context, session, th.BasicChannel.Id, model.PermissionReadChannel))
    -	})
    -
    -	t.Run("basic user can access archived channel if setting is on", func(t *testing.T) {
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = true
    -		})
    +	t.Run("basic user can access archived channel", func(t *testing.T) {
     		err := th.App.DeleteChannel(th.Context, th.BasicChannel, th.SystemAdminUser.Id)
     		require.Nil(t, err)
     		assert.True(t, th.App.SessionHasPermissionToChannel(th.Context, session, th.BasicChannel.Id, model.PermissionReadChannel))
    @@ -288,15 +276,6 @@ func TestSessionHasPermissionToChannel(t *testing.T) {
     		// If there's an error returned from the GetChannel call the code should continue to cascade and since there
     		// are no session level permissions in this test case, the permission should be denied.
     		assert.False(t, th.App.SessionHasPermissionToChannel(th.Context, session, th.BasicUser.Id, model.PermissionAddReaction))
    -
    -		// MM-63624, check with TeamSettings.ExperimentalViewArchivedChannels off
    -		th.App.Srv().SetStore(mainHelper.GetStore())
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = false
    -		})
    -
    -		th.App.Srv().SetStore(&mockStore)
    -		assert.False(t, th.App.SessionHasPermissionToChannel(th.Context, session, th.BasicUser.Id, model.PermissionAddReaction))
     	})
     }
     
    @@ -332,13 +311,10 @@ func TestSessionHasPermissionToChannels(t *testing.T) {
     		assert.False(t, th.App.SessionHasPermissionToChannels(th.Context, session, allChannels, model.PermissionReadChannel))
     	})
     
    -	t.Run("basic user can access archived channel if setting is on", func(t *testing.T) {
    +	t.Run("basic user can access archived channel", func(t *testing.T) {
     		session := model.Session{
     			UserId: th.BasicUser.Id,
     		}
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = true
    -		})
     
     		newChannel := th.CreateChannel(th.Context, th.BasicTeam)
     		_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, newChannel, false)
    @@ -349,49 +325,10 @@ func TestSessionHasPermissionToChannels(t *testing.T) {
     		assert.True(t, th.App.SessionHasPermissionToChannels(th.Context, session, []string{newChannel.Id}, model.PermissionReadChannel))
     	})
     
    -	t.Run("basic user cannot access archived channel if setting is off", func(t *testing.T) {
    +	t.Run("basic user can access mixed archived and non-archived channels", func(t *testing.T) {
     		session := model.Session{
     			UserId: th.BasicUser.Id,
     		}
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = false
    -		})
    -
    -		newChannel := th.CreateChannel(th.Context, th.BasicTeam)
    -		_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, newChannel, false)
    -		assert.Nil(t, appErr)
    -
    -		err := th.App.DeleteChannel(th.Context, newChannel, th.SystemAdminUser.Id)
    -		require.Nil(t, err)
    -		assert.False(t, th.App.SessionHasPermissionToChannels(th.Context, session, []string{newChannel.Id}, model.PermissionReadChannel))
    -	})
    -
    -	t.Run("basic user cannot access mixed archived and non-archived channels if setting is off", func(t *testing.T) {
    -		session := model.Session{
    -			UserId: th.BasicUser.Id,
    -		}
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = false
    -		})
    -
    -		archivedChannel := th.CreateChannel(th.Context, th.BasicTeam)
    -		_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, archivedChannel, false)
    -		assert.Nil(t, appErr)
    -
    -		err := th.App.DeleteChannel(th.Context, archivedChannel, th.SystemAdminUser.Id)
    -		require.Nil(t, err)
    -
    -		mixedChannels := []string{th.BasicChannel.Id, archivedChannel.Id}
    -		assert.False(t, th.App.SessionHasPermissionToChannels(th.Context, session, mixedChannels, model.PermissionReadChannel))
    -	})
    -
    -	t.Run("basic user can access mixed archived and non-archived channels if setting is on", func(t *testing.T) {
    -		session := model.Session{
    -			UserId: th.BasicUser.Id,
    -		}
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = true
    -		})
     
     		archivedChannel := th.CreateChannel(th.Context, th.BasicTeam)
     		_, appErr := th.App.AddUserToChannel(th.Context, th.BasicUser, archivedChannel, false)
    @@ -438,14 +375,6 @@ func TestSessionHasPermissionToChannels(t *testing.T) {
     			UserId: th.BasicUser.Id,
     		}
     		assert.False(t, th.App.SessionHasPermissionToChannels(th.Context, session, allChannels, model.PermissionReadChannel))
    -
    -		// MM-63624, check with TeamSettings.ExperimentalViewArchivedChannels off
    -		th.App.Srv().SetStore(mainHelper.GetStore())
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = false
    -		})
    -		th.App.Srv().SetStore(&mockStore)
    -		assert.False(t, th.App.SessionHasPermissionToChannels(th.Context, session, allChannels, model.PermissionReadChannel))
     	})
     }
     
    @@ -789,7 +718,6 @@ func TestHasPermissionToReadChannel(t *testing.T) {
     
     	ttcc := []struct {
     		name                    string
    -		configViewArchived      bool
     		configComplianceEnabled bool
     		channelDeleted          bool
     		canReadChannel          bool
    @@ -798,18 +726,16 @@ func TestHasPermissionToReadChannel(t *testing.T) {
     		expected                bool
     	}{
     		{
    -			name:                    "Cannot read archived channels if the config doesn't allow it",
    -			configViewArchived:      false,
    +			name:                    "Can read archived channels",
     			configComplianceEnabled: true,
     			channelDeleted:          true,
     			canReadChannel:          true,
     			channelIsOpen:           true,
     			canReadPublicChannel:    true,
    -			expected:                false,
    +			expected:                true,
     		},
     		{
     			name:                    "Can read if it has permissions to read",
    -			configViewArchived:      false,
     			configComplianceEnabled: true,
     			channelDeleted:          false,
     			canReadChannel:          true,
    @@ -819,7 +745,6 @@ func TestHasPermissionToReadChannel(t *testing.T) {
     		},
     		{
     			name:                    "Cannot read private channels if it has no permission",
    -			configViewArchived:      false,
     			configComplianceEnabled: false,
     			channelDeleted:          false,
     			canReadChannel:          false,
    @@ -829,7 +754,6 @@ func TestHasPermissionToReadChannel(t *testing.T) {
     		},
     		{
     			name:                    "Cannot read open channels if compliance is enabled",
    -			configViewArchived:      false,
     			configComplianceEnabled: true,
     			channelDeleted:          false,
     			canReadChannel:          false,
    @@ -839,7 +763,6 @@ func TestHasPermissionToReadChannel(t *testing.T) {
     		},
     		{
     			name:                    "Cannot read open channels if it has no team permissions",
    -			configViewArchived:      false,
     			configComplianceEnabled: false,
     			channelDeleted:          false,
     			canReadChannel:          false,
    @@ -849,7 +772,6 @@ func TestHasPermissionToReadChannel(t *testing.T) {
     		},
     		{
     			name:                    "Can read open channels if it has team permissions and compliance is not enabled",
    -			configViewArchived:      false,
     			configComplianceEnabled: false,
     			channelDeleted:          false,
     			canReadChannel:          false,
    @@ -862,9 +784,7 @@ func TestHasPermissionToReadChannel(t *testing.T) {
     	for _, tc := range ttcc {
     		t.Run(tc.name, func(t *testing.T) {
     			th.App.UpdateConfig(func(cfg *model.Config) {
    -				configViewArchived := tc.configViewArchived
     				configComplianceEnabled := tc.configComplianceEnabled
    -				cfg.TeamSettings.ExperimentalViewArchivedChannels = &configViewArchived
     				cfg.ComplianceSettings.Enable = &configComplianceEnabled
     			})
     
    @@ -929,18 +849,7 @@ func TestSessionHasPermissionToChannelByPost(t *testing.T) {
     		require.Equal(t, false, th.App.SessionHasPermissionToChannelByPost(*session2, post.Id, model.PermissionReadChannel))
     	})
     
    -	t.Run("read archived channel - setting off", func(t *testing.T) {
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			cfg.TeamSettings.ExperimentalViewArchivedChannels = model.NewPointer(false)
    -		})
    -		require.Equal(t, false, th.App.SessionHasPermissionToChannelByPost(*session, archivedPost.Id, model.PermissionReadChannel))
    -		require.Equal(t, false, th.App.SessionHasPermissionToChannelByPost(*session2, archivedPost.Id, model.PermissionReadChannel))
    -	})
    -
    -	t.Run("read archived channel - setting on", func(t *testing.T) {
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			cfg.TeamSettings.ExperimentalViewArchivedChannels = model.NewPointer(true)
    -		})
    +	t.Run("read archived channel", func(t *testing.T) {
     		require.Equal(t, true, th.App.SessionHasPermissionToChannelByPost(*session, archivedPost.Id, model.PermissionReadChannel))
     		require.Equal(t, false, th.App.SessionHasPermissionToChannelByPost(*session2, archivedPost.Id, model.PermissionReadChannel))
     	})
    @@ -981,18 +890,7 @@ func TestHasPermissionToChannelByPost(t *testing.T) {
     		require.Equal(t, false, th.App.HasPermissionToChannelByPost(th.Context, th.BasicUser2.Id, post.Id, model.PermissionReadChannel))
     	})
     
    -	t.Run("read archived channel - setting off", func(t *testing.T) {
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			cfg.TeamSettings.ExperimentalViewArchivedChannels = model.NewPointer(false)
    -		})
    -		require.Equal(t, false, th.App.HasPermissionToChannelByPost(th.Context, th.BasicUser.Id, archivedPost.Id, model.PermissionReadChannel))
    -		require.Equal(t, false, th.App.HasPermissionToChannelByPost(th.Context, th.BasicUser2.Id, archivedPost.Id, model.PermissionReadChannel))
    -	})
    -
    -	t.Run("read archived channel - setting on", func(t *testing.T) {
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			cfg.TeamSettings.ExperimentalViewArchivedChannels = model.NewPointer(true)
    -		})
    +	t.Run("read archived channel", func(t *testing.T) {
     		require.Equal(t, true, th.App.HasPermissionToChannelByPost(th.Context, th.BasicUser.Id, archivedPost.Id, model.PermissionReadChannel))
     		require.Equal(t, false, th.App.HasPermissionToChannelByPost(th.Context, th.BasicUser2.Id, archivedPost.Id, model.PermissionReadChannel))
     	})
    
  • server/channels/app/channel.go+5 5 modified
    @@ -3056,7 +3056,7 @@ func (a *App) sendWebSocketPostUnreadEvent(c request.CTX, channelUnread *model.C
     }
     
     func (a *App) AutocompleteChannels(c request.CTX, userID, term string) (model.ChannelListWithTeamData, *model.AppError) {
    -	includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
    +	includeDeleted := true
     	term = strings.TrimSpace(term)
     
     	user, appErr := a.GetUser(userID)
    @@ -3073,7 +3073,7 @@ func (a *App) AutocompleteChannels(c request.CTX, userID, term string) (model.Ch
     }
     
     func (a *App) AutocompleteChannelsForTeam(c request.CTX, teamID, userID, term string) (model.ChannelList, *model.AppError) {
    -	includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
    +	includeDeleted := true
     	term = strings.TrimSpace(term)
     
     	user, appErr := a.GetUser(userID)
    @@ -3090,7 +3090,7 @@ func (a *App) AutocompleteChannelsForTeam(c request.CTX, teamID, userID, term st
     }
     
     func (a *App) AutocompleteChannelsForSearch(c request.CTX, teamID string, userID string, term string) (model.ChannelList, *model.AppError) {
    -	includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
    +	includeDeleted := true
     
     	term = strings.TrimSpace(term)
     
    @@ -3140,7 +3140,7 @@ func (a *App) SearchAllChannels(c request.CTX, term string, opts model.ChannelSe
     }
     
     func (a *App) SearchChannels(c request.CTX, teamID string, term string) (model.ChannelList, *model.AppError) {
    -	includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
    +	includeDeleted := true
     
     	term = strings.TrimSpace(term)
     
    @@ -3164,7 +3164,7 @@ func (a *App) SearchArchivedChannels(c request.CTX, teamID string, term string,
     }
     
     func (a *App) SearchChannelsForUser(c request.CTX, userID, teamID, term string) (model.ChannelList, *model.AppError) {
    -	includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
    +	includeDeleted := true
     
     	term = strings.TrimSpace(term)
     
    
  • server/channels/app/file.go+1 2 modified
    @@ -1440,7 +1440,6 @@ func populateZipfile(w *zip.Writer, fileDatas []model.FileData) error {
     
     func (a *App) SearchFilesInTeamForUser(c request.CTX, terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.FileInfoList, *model.AppError) {
     	paramsList := model.ParseSearchParams(strings.TrimSpace(terms), timeZoneOffset)
    -	includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels
     
     	if !*a.Config().ServiceSettings.EnableFileSearch {
     		return nil, model.NewAppError("SearchFilesInTeamForUser", "store.sql_file_info.search.disabled", nil, fmt.Sprintf("teamId=%v userId=%v", teamId, userId), http.StatusNotImplemented)
    @@ -1450,7 +1449,7 @@ func (a *App) SearchFilesInTeamForUser(c request.CTX, terms string, userId strin
     
     	for _, params := range paramsList {
     		params.OrTerms = isOrSearch
    -		params.IncludeDeletedChannels = includeDeleted
    +		params.IncludeDeletedChannels = includeDeletedChannels
     		// Don't allow users to search for "*"
     		if params.Terms != "*" {
     			// Convert channel names to channel IDs
    
  • server/channels/app/post.go+1 2 modified
    @@ -1716,7 +1716,6 @@ func (a *App) SearchPostsInTeam(teamID string, paramsList []*model.SearchParams)
     func (a *App) SearchPostsForUser(c request.CTX, terms string, userID string, teamID string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.PostSearchResults, *model.AppError) {
     	var postSearchResults *model.PostSearchResults
     	paramsList := model.ParseSearchParams(strings.TrimSpace(terms), timeZoneOffset)
    -	includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels
     
     	if !*a.Config().ServiceSettings.EnablePostSearch {
     		return nil, model.NewAppError("SearchPostsForUser", "store.sql_post.search.disabled", nil, fmt.Sprintf("teamId=%v userId=%v", teamID, userID), http.StatusNotImplemented)
    @@ -1726,7 +1725,7 @@ func (a *App) SearchPostsForUser(c request.CTX, terms string, userID string, tea
     
     	for _, params := range paramsList {
     		params.OrTerms = isOrSearch
    -		params.IncludeDeletedChannels = includeDeleted
    +		params.IncludeDeletedChannels = includeDeletedChannels
     		// Don't allow users to search for "*"
     		if params.Terms != "*" {
     			// TODO: we have to send channel ids
    
  • server/channels/app/post_metadata_test.go+1 41 modified
    @@ -3025,14 +3025,7 @@ func TestSanitizePostMetadataForUserAndChannel(t *testing.T) {
     		assert.Len(t, actual.Metadata.Embeds, 0)
     	})
     
    -	t.Run("should not preview for archived channels", func(t *testing.T) {
    -		experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
    -		defer func() {
    -			th.App.UpdateConfig(func(cfg *model.Config) {
    -				cfg.TeamSettings.ExperimentalViewArchivedChannels = &experimentalViewArchivedChannels
    -			})
    -		}()
    -
    +	t.Run("channel previews always work for archived channels", func(t *testing.T) {
     		publicChannel, err := th.App.CreateChannel(th.Context, &model.Channel{
     			Name:      model.NewId(),
     			Type:      model.ChannelTypeOpen,
    @@ -3072,19 +3065,8 @@ func TestSanitizePostMetadataForUserAndChannel(t *testing.T) {
     
     		previewedPost := model.NewPreviewPost(post, th.BasicTeam, publicChannel)
     
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = true
    -		})
    -
     		actual := th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, publicChannel, th.BasicUser.Id)
     		assert.NotNil(t, actual.Metadata.Embeds[0].Data)
    -
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = false
    -		})
    -
    -		actual = th.App.sanitizePostMetadataForUserAndChannel(th.Context, post, previewedPost, publicChannel, th.BasicUser.Id)
    -		assert.Len(t, actual.Metadata.Embeds, 0)
     	})
     }
     
    @@ -3253,34 +3235,12 @@ func TestSanitizePostMetadataForUser(t *testing.T) {
     			},
     		}
     
    -		experimentalViewArchivedChannels := *th.App.Config().TeamSettings.ExperimentalViewArchivedChannels
    -		defer func() {
    -			th.App.UpdateConfig(func(cfg *model.Config) {
    -				cfg.TeamSettings.ExperimentalViewArchivedChannels = &experimentalViewArchivedChannels
    -			})
    -		}()
    -
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = true
    -		})
    -
     		sanitizedPost, err := th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id)
     		require.Nil(t, err)
     		require.NotNil(t, sanitizedPost)
     
     		require.Equal(t, 2, len(sanitizedPost.Metadata.Embeds))
     		require.Equal(t, model.PostEmbedPermalink, sanitizedPost.Metadata.Embeds[0].Type)
    -
    -		th.App.UpdateConfig(func(cfg *model.Config) {
    -			*cfg.TeamSettings.ExperimentalViewArchivedChannels = false
    -		})
    -
    -		sanitizedPost, err = th.App.SanitizePostMetadataForUser(th.Context, post, th.BasicUser.Id)
    -		require.Nil(t, err)
    -		require.NotNil(t, sanitizedPost)
    -
    -		require.Equal(t, 1, len(sanitizedPost.Metadata.Embeds))
    -		require.Equal(t, model.PostEmbedLink, sanitizedPost.Metadata.Embeds[0].Type)
     	})
     }
     
    
  • server/channels/store/retrylayer/retrylayer.go+2 2 modified
    @@ -2031,11 +2031,11 @@ func (s *RetryLayerChannelStore) GetMemberCountsByGroup(ctx context.Context, cha
     
     }
     
    -func (s *RetryLayerChannelStore) GetMemberForPost(postID string, userID string, includeArchivedChannels bool) (*model.ChannelMember, error) {
    +func (s *RetryLayerChannelStore) GetMemberForPost(postID string, userID string) (*model.ChannelMember, error) {
     
     	tries := 0
     	for {
    -		result, err := s.ChannelStore.GetMemberForPost(postID, userID, includeArchivedChannels)
    +		result, err := s.ChannelStore.GetMemberForPost(postID, userID)
     		if err == nil {
     			return result, nil
     		}
    
  • server/channels/store/sqlstore/channel_store.go+1 4 modified
    @@ -2169,7 +2169,7 @@ func (s SqlChannelStore) GetMemberLastViewedAt(ctx context.Context, channelID st
     func (s SqlChannelStore) InvalidateAllChannelMembersForUser(userId string) {
     }
     
    -func (s SqlChannelStore) GetMemberForPost(postId string, userId string, includeArchivedChannels bool) (*model.ChannelMember, error) {
    +func (s SqlChannelStore) GetMemberForPost(postId string, userId string) (*model.ChannelMember, error) {
     	var dbMember channelMemberWithSchemeRoles
     	query := `
     		SELECT
    @@ -2210,9 +2210,6 @@ func (s SqlChannelStore) GetMemberForPost(postId string, userId string, includeA
     		AND
     			Posts.Id = ?`
     
    -	if !includeArchivedChannels {
    -		query += " AND Channels.DeleteAt = 0"
    -	}
     	if err := s.GetReplica().Get(&dbMember, query, userId, postId); err != nil {
     		return nil, errors.Wrapf(err, "failed to get ChannelMember with postId=%s and userId=%s", postId, userId)
     	}
    
  • server/channels/store/store.go+1 1 modified
    @@ -242,7 +242,7 @@ type ChannelStore interface {
     	InvalidateAllChannelMembersForUser(userID string)
     	GetAllChannelMembersNotifyPropsForChannel(channelID string, allowFromCache bool) (map[string]model.StringMap, error)
     	InvalidateCacheForChannelMembersNotifyProps(channelID string)
    -	GetMemberForPost(postID string, userID string, includeArchivedChannels bool) (*model.ChannelMember, error)
    +	GetMemberForPost(postID string, userID string) (*model.ChannelMember, error)
     	InvalidateMemberCount(channelID string)
     	GetMemberCountFromCache(channelID string) int64
     	GetFileCount(channelID string) (int64, error)
    
  • server/channels/store/storetest/channel_store.go+2 2 modified
    @@ -5388,11 +5388,11 @@ func testChannelStoreGetMemberForPost(t *testing.T, rctx request.CTX, ss store.S
     	})
     	require.NoError(t, nErr)
     
    -	r1, err := ss.Channel().GetMemberForPost(p1.Id, m1.UserId, false)
    +	r1, err := ss.Channel().GetMemberForPost(p1.Id, m1.UserId)
     	require.NoError(t, err, err)
     	require.Equal(t, channelMemberToJSON(t, m1), channelMemberToJSON(t, r1), "invalid returned channel member")
     
    -	_, err = ss.Channel().GetMemberForPost(p1.Id, model.NewId(), false)
    +	_, err = ss.Channel().GetMemberForPost(p1.Id, model.NewId())
     	require.Error(t, err, "shouldn't have returned a member")
     }
     
    
  • server/channels/store/storetest/mocks/ChannelStore.go+9 9 modified
    @@ -1522,29 +1522,29 @@ func (_m *ChannelStore) GetMemberCountsByGroup(ctx context.Context, channelID st
     	return r0, r1
     }
     
    -// GetMemberForPost provides a mock function with given fields: postID, userID, includeArchivedChannels
    -func (_m *ChannelStore) GetMemberForPost(postID string, userID string, includeArchivedChannels bool) (*model.ChannelMember, error) {
    -	ret := _m.Called(postID, userID, includeArchivedChannels)
    +// GetMemberForPost provides a mock function with given fields: postID, userID
    +func (_m *ChannelStore) GetMemberForPost(postID string, userID string) (*model.ChannelMember, error) {
    +	ret := _m.Called(postID, userID)
     
     	if len(ret) == 0 {
     		panic("no return value specified for GetMemberForPost")
     	}
     
     	var r0 *model.ChannelMember
     	var r1 error
    -	if rf, ok := ret.Get(0).(func(string, string, bool) (*model.ChannelMember, error)); ok {
    -		return rf(postID, userID, includeArchivedChannels)
    +	if rf, ok := ret.Get(0).(func(string, string) (*model.ChannelMember, error)); ok {
    +		return rf(postID, userID)
     	}
    -	if rf, ok := ret.Get(0).(func(string, string, bool) *model.ChannelMember); ok {
    -		r0 = rf(postID, userID, includeArchivedChannels)
    +	if rf, ok := ret.Get(0).(func(string, string) *model.ChannelMember); ok {
    +		r0 = rf(postID, userID)
     	} else {
     		if ret.Get(0) != nil {
     			r0 = ret.Get(0).(*model.ChannelMember)
     		}
     	}
     
    -	if rf, ok := ret.Get(1).(func(string, string, bool) error); ok {
    -		r1 = rf(postID, userID, includeArchivedChannels)
    +	if rf, ok := ret.Get(1).(func(string, string) error); ok {
    +		r1 = rf(postID, userID)
     	} else {
     		r1 = ret.Error(1)
     	}
    
  • server/channels/store/timerlayer/timerlayer.go+2 2 modified
    @@ -1697,10 +1697,10 @@ func (s *TimerLayerChannelStore) GetMemberCountsByGroup(ctx context.Context, cha
     	return result, err
     }
     
    -func (s *TimerLayerChannelStore) GetMemberForPost(postID string, userID string, includeArchivedChannels bool) (*model.ChannelMember, error) {
    +func (s *TimerLayerChannelStore) GetMemberForPost(postID string, userID string) (*model.ChannelMember, error) {
     	start := time.Now()
     
    -	result, err := s.ChannelStore.GetMemberForPost(postID, userID, includeArchivedChannels)
    +	result, err := s.ChannelStore.GetMemberForPost(postID, userID)
     
     	elapsed := float64(time.Since(start)) / float64(time.Second)
     	if s.Root.Metrics != nil {
    
  • server/config/client.go+0 1 modified
    @@ -21,7 +21,6 @@ func GenerateClientConfig(c *model.Config, telemetryID string, license *model.Li
     	props["TeammateNameDisplay"] = *c.TeamSettings.TeammateNameDisplay
     	props["LockTeammateNameDisplay"] = strconv.FormatBool(*c.TeamSettings.LockTeammateNameDisplay)
     	props["ExperimentalPrimaryTeam"] = *c.TeamSettings.ExperimentalPrimaryTeam
    -	props["ExperimentalViewArchivedChannels"] = strconv.FormatBool(*c.TeamSettings.ExperimentalViewArchivedChannels)
     	props["EnableJoinLeaveMessageByDefault"] = strconv.FormatBool(*c.TeamSettings.EnableJoinLeaveMessageByDefault)
     
     	props["EnableBotAccountCreation"] = strconv.FormatBool(*c.ServiceSettings.EnableBotAccountCreation)
    
  • server/i18n/en.json+4 12 modified
    @@ -4466,18 +4466,6 @@
         "id": "api.user.verify_email.token_parse.error",
         "translation": "Failed to parse token data from email verification"
       },
    -  {
    -    "id": "api.user.view_archived_channels.get_posts_for_channel.app_error",
    -    "translation": "Cannot retrieve posts for an archived channel"
    -  },
    -  {
    -    "id": "api.user.view_archived_channels.get_users_in_channel.app_error",
    -    "translation": "Cannot retrieve users for an archived channel"
    -  },
    -  {
    -    "id": "api.user.view_archived_channels.list_channel_bookmarks_for_channel.app_error",
    -    "translation": "Cannot retrieve bookmarks for an archived channel"
    -  },
       {
         "id": "api.web_socket.connect.upgrade.app_error",
         "translation": "URL Blocked because of CORS. Url: {{.BlockedOrigin}}"
    @@ -9176,6 +9164,10 @@
         "id": "model.config.is_valid.experimental_audit_settings.file_name_is_directory",
         "translation": "The file name must not be a directory."
       },
    +  {
    +    "id": "model.config.is_valid.experimental_view_archived_channels.app_error",
    +    "translation": "Hiding archived channels is no longer supported. Make these channels private and remove members instead."
    +  },
       {
         "id": "model.config.is_valid.export.directory.app_error",
         "translation": "Value for Directory should not be empty."
    
  • server/platform/services/telemetry/telemetry.go+0 1 modified
    @@ -584,7 +584,6 @@ func (ts *TelemetryService) trackConfig() {
     		"max_users_per_team":                      *cfg.TeamSettings.MaxUsersPerTeam,
     		"max_channels_per_team":                   *cfg.TeamSettings.MaxChannelsPerTeam,
     		"teammate_name_display":                   *cfg.TeamSettings.TeammateNameDisplay,
    -		"experimental_view_archived_channels":     *cfg.TeamSettings.ExperimentalViewArchivedChannels,
     		"lock_teammate_name_display":              *cfg.TeamSettings.LockTeammateNameDisplay,
     		"isdefault_site_name":                     isDefault(cfg.TeamSettings.SiteName, "Mattermost"),
     		"isdefault_custom_brand_text":             isDefault(*cfg.TeamSettings.CustomBrandText, model.TeamSettingsDefaultCustomBrandText),
    
  • server/public/model/config.go+15 10 modified
    @@ -2395,16 +2395,17 @@ type TeamSettings struct {
     	RestrictDirectMessage           *string `access:"site_users_and_teams"`
     	EnableLastActiveTime            *bool   `access:"site_users_and_teams"`
     	// In seconds.
    -	UserStatusAwayTimeout               *int64   `access:"experimental_features"`
    -	MaxChannelsPerTeam                  *int64   `access:"site_users_and_teams"`
    -	MaxNotificationsPerChannel          *int64   `access:"environment_push_notification_server"`
    -	EnableConfirmNotificationsToChannel *bool    `access:"site_notifications"`
    -	TeammateNameDisplay                 *string  `access:"site_users_and_teams"`
    -	ExperimentalViewArchivedChannels    *bool    `access:"experimental_features,site_users_and_teams"`
    -	ExperimentalEnableAutomaticReplies  *bool    `access:"experimental_features"`
    -	LockTeammateNameDisplay             *bool    `access:"site_users_and_teams"`
    -	ExperimentalPrimaryTeam             *string  `access:"experimental_features"`
    -	ExperimentalDefaultChannels         []string `access:"experimental_features"`
    +	UserStatusAwayTimeout               *int64  `access:"experimental_features"`
    +	MaxChannelsPerTeam                  *int64  `access:"site_users_and_teams"`
    +	MaxNotificationsPerChannel          *int64  `access:"environment_push_notification_server"`
    +	EnableConfirmNotificationsToChannel *bool   `access:"site_notifications"`
    +	TeammateNameDisplay                 *string `access:"site_users_and_teams"`
    +	// Deprecated: This field is no longer in use, and should always be true.
    +	ExperimentalViewArchivedChannels   *bool    `access:"experimental_features,site_users_and_teams"`
    +	ExperimentalEnableAutomaticReplies *bool    `access:"experimental_features"`
    +	LockTeammateNameDisplay            *bool    `access:"site_users_and_teams"`
    +	ExperimentalPrimaryTeam            *string  `access:"experimental_features"`
    +	ExperimentalDefaultChannels        []string `access:"experimental_features"`
     }
     
     func (s *TeamSettings) SetDefaults() {
    @@ -4141,6 +4142,10 @@ func (s *TeamSettings) isValid() *AppError {
     		return NewAppError("Config.IsValid", "model.config.is_valid.sitename_length.app_error", map[string]any{"MaxLength": SitenameMaxLength}, "", http.StatusBadRequest)
     	}
     
    +	if !*s.ExperimentalViewArchivedChannels {
    +		return NewAppError("Config.IsValid", "model.config.is_valid.experimental_view_archived_channels.app_error", nil, "", http.StatusBadRequest)
    +	}
    +
     	return nil
     }
     
    
  • webapp/channels/src/actions/command.test.js+3 5 modified
    @@ -30,11 +30,6 @@ const initialState = {
                     },
                 },
             },
    -        general: {
    -            config: {
    -                ExperimentalViewArchivedChannels: 'false',
    -            },
    -        },
             posts: {
                 posts: {
                     root_id: {id: 'root_id', channel_id: '123'},
    @@ -124,6 +119,9 @@ const initialState = {
                 },
                 pluginEnabled: true,
             },
    +        general: {
    +            config: {},
    +        },
         },
         views: {
             rhs: {
    
  • webapp/channels/src/actions/views/channel.ts+1 12 modified
    @@ -32,7 +32,6 @@ import {
         isManuallyUnread,
         getCurrentChannelId,
     } from 'mattermost-redux/selectors/entities/channels';
    -import {getConfig} from 'mattermost-redux/selectors/entities/general';
     import {getMostRecentPostIdInChannel, getPost} from 'mattermost-redux/selectors/entities/posts';
     import {
         getCurrentRelativeTeamUrl,
    @@ -50,7 +49,7 @@ import {openDirectChannelToUserId} from 'actions/channel_actions';
     import {loadCustomStatusEmojisForPostList} from 'actions/emoji_actions';
     import {closeRightHandSide} from 'actions/views/rhs';
     import {markThreadAsRead} from 'actions/views/threads';
    -import {getLastViewedChannelName, getPenultimateViewedChannelName} from 'selectors/local_storage';
    +import {getLastViewedChannelName} from 'selectors/local_storage';
     import {getSelectedPost, getSelectedPostId} from 'selectors/rhs';
     import {getLastPostsApiTimeForChannel} from 'selectors/views/channel';
     import {getSelectedThreadIdInCurrentTeam} from 'selectors/views/threads';
    @@ -535,16 +534,6 @@ export function deleteChannel(channelId: string): ActionFuncAsync<boolean> {
                 return {data: false};
             }
     
    -        const canViewArchivedChannels = getConfig(state).ExperimentalViewArchivedChannels === 'true';
    -        const currentTeamDetails = getCurrentTeam(state);
    -        const penultimateViewedChannelName = getPenultimateViewedChannelName(state) ||
    -            getRedirectChannelNameForTeam(state, getCurrentTeamId(state));
    -
    -        // Handle redirection before deletion if needed
    -        if (!canViewArchivedChannels && penultimateViewedChannelName && currentTeamDetails) {
    -            getHistory().push('/' + currentTeamDetails.name + '/channels/' + penultimateViewedChannelName);
    -        }
    -
             // Call the delete channel action
             const res = await dispatch(deleteChannelRedux(channelId));
             if (res.error) {
    
  • webapp/channels/src/actions/views/rhs.test.ts+6 11 modified
    @@ -91,11 +91,6 @@ jest.mock('actions/telemetry_actions.jsx', () => ({
     describe('rhs view actions', () => {
         const initialState = {
             entities: {
    -            general: {
    -                config: {
    -                    ExperimentalViewArchivedChannels: 'false',
    -                },
    -            },
                 channels: {
                     currentChannelId,
                 },
    @@ -234,8 +229,8 @@ describe('rhs view actions', () => {
                 store.dispatch(performSearch(terms, currentTeamId, false));
     
                 const compareStore = mockStore(initialState);
    -            compareStore.dispatch(SearchActions.searchPostsWithParams(currentTeamId, {include_deleted_channels: false, terms, is_or_search: false, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
    -            compareStore.dispatch(SearchActions.searchFilesWithParams(currentTeamId, {include_deleted_channels: false, terms, is_or_search: false, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
    +            compareStore.dispatch(SearchActions.searchPostsWithParams(currentTeamId, {include_deleted_channels: true, terms, is_or_search: false, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
    +            compareStore.dispatch(SearchActions.searchFilesWithParams(currentTeamId, {include_deleted_channels: true, terms, is_or_search: false, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
     
                 expect(store.getActions()).toEqual(compareStore.getActions());
             });
    @@ -257,8 +252,8 @@ describe('rhs view actions', () => {
     
                 const filesExtTerms = '@here test search ext:txt ext:jpeg';
                 const compareStore = mockStore(initialState);
    -            compareStore.dispatch(SearchActions.searchPostsWithParams(currentTeamId, {include_deleted_channels: false, terms, is_or_search: false, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
    -            compareStore.dispatch(SearchActions.searchFilesWithParams(currentTeamId, {include_deleted_channels: false, terms: filesExtTerms, is_or_search: false, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
    +            compareStore.dispatch(SearchActions.searchPostsWithParams(currentTeamId, {include_deleted_channels: true, terms, is_or_search: false, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
    +            compareStore.dispatch(SearchActions.searchFilesWithParams(currentTeamId, {include_deleted_channels: true, terms: filesExtTerms, is_or_search: false, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
     
                 expect(store.getActions()).toEqual(compareStore.getActions());
             });
    @@ -269,8 +264,8 @@ describe('rhs view actions', () => {
     
                 const mentionsQuotedTerms = `"@here" "test" "search" "${currentUsername}" "@${currentUsername}" "${currentUserFirstName}" "custom-hyphenated-term"`;
                 const compareStore = mockStore(initialState);
    -            compareStore.dispatch(SearchActions.searchPostsWithParams('', {include_deleted_channels: false, terms: mentionsQuotedTerms, is_or_search: true, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
    -            compareStore.dispatch(SearchActions.searchFilesWithParams('', {include_deleted_channels: false, terms, is_or_search: true, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
    +            compareStore.dispatch(SearchActions.searchPostsWithParams('', {include_deleted_channels: true, terms: mentionsQuotedTerms, is_or_search: true, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
    +            compareStore.dispatch(SearchActions.searchFilesWithParams('', {include_deleted_channels: true, terms, is_or_search: true, time_zone_offset: timeZoneOffset, page: 0, per_page: 20}));
     
                 expect(store.getActions()).toEqual(compareStore.getActions());
             });
    
  • webapp/channels/src/actions/views/rhs.ts+2 5 modified
    @@ -18,7 +18,6 @@ import {
         searchFilesWithParams,
     } from 'mattermost-redux/actions/search';
     import {getCurrentChannelId, getCurrentChannelNameForSearchShortcut, getChannel as getChannelSelector} from 'mattermost-redux/selectors/entities/channels';
    -import {getConfig} from 'mattermost-redux/selectors/entities/general';
     import {getLatestInteractablePostId, getPost} from 'mattermost-redux/selectors/entities/posts';
     import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
     import {getCurrentTimezone} from 'mattermost-redux/selectors/entities/timezone';
    @@ -211,8 +210,6 @@ function updateSearchResultsType(searchType: string) {
     export function performSearch(terms: string, teamId: string, isMentionSearch?: boolean): ThunkActionFunc<unknown> {
         return (dispatch, getState) => {
             let searchTerms = terms;
    -        const config = getConfig(getState());
    -        const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
             const extensionsFilters = getFilesSearchExtFilter(getState());
     
             const extensions = extensionsFilters?.map((ext) => `ext:${ext}`).join(' ');
    @@ -234,8 +231,8 @@ export function performSearch(terms: string, teamId: string, isMentionSearch?: b
             // timezone offset in seconds
             const userCurrentTimezone = getCurrentTimezone(getState());
             const timezoneOffset = ((userCurrentTimezone && (userCurrentTimezone.length > 0)) ? getUtcOffsetForTimeZone(userCurrentTimezone) : getBrowserUtcOffset()) * 60;
    -        const messagesPromise = dispatch(searchPostsWithParams(teamId, {terms: searchTerms, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
    -        const filesPromise = dispatch(searchFilesWithParams(teamId, {terms: termsWithExtensionsFilters, is_or_search: Boolean(isMentionSearch), include_deleted_channels: viewArchivedChannels, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
    +        const messagesPromise = dispatch(searchPostsWithParams(teamId, {terms: searchTerms, is_or_search: Boolean(isMentionSearch), include_deleted_channels: true, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
    +        const filesPromise = dispatch(searchFilesWithParams(teamId, {terms: termsWithExtensionsFilters, is_or_search: Boolean(isMentionSearch), include_deleted_channels: true, time_zone_offset: timezoneOffset, page: 0, per_page: 20}));
             return Promise.all([filesPromise, messagesPromise]);
         };
     }
    
  • webapp/channels/src/actions/websocket_actions.jsx+2 17 modified
    @@ -89,7 +89,6 @@ import {haveISystemPermission, haveITeamPermission} from 'mattermost-redux/selec
     import {
         getTeamIdByChannelId,
         getMyTeams,
    -    getCurrentRelativeTeamUrl,
         getCurrentTeamId,
         getCurrentTeamUrl,
         getTeam,
    @@ -1263,25 +1262,11 @@ function handleChannelCreatedEvent(msg) {
     }
     
     function handleChannelDeletedEvent(msg) {
    -    const state = getState();
    -    const config = getConfig(state);
    -    const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
    -    if (getCurrentChannelId(state) === msg.data.channel_id && !viewArchivedChannels) {
    -        const teamUrl = getCurrentRelativeTeamUrl(state);
    -        const currentTeamId = getCurrentTeamId(state);
    -        const redirectChannel = getRedirectChannelNameForTeam(state, currentTeamId);
    -        getHistory().push(teamUrl + '/channels/' + redirectChannel);
    -    }
    -
    -    dispatch({type: ChannelTypes.RECEIVED_CHANNEL_DELETED, data: {id: msg.data.channel_id, team_id: msg.broadcast.team_id, deleteAt: msg.data.delete_at, viewArchivedChannels}});
    +    dispatch({type: ChannelTypes.RECEIVED_CHANNEL_DELETED, data: {id: msg.data.channel_id, team_id: msg.broadcast.team_id, deleteAt: msg.data.delete_at, viewArchivedChannels: true}});
     }
     
     function handleChannelUnarchivedEvent(msg) {
    -    const state = getState();
    -    const config = getConfig(state);
    -    const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
    -
    -    dispatch({type: ChannelTypes.RECEIVED_CHANNEL_UNARCHIVED, data: {id: msg.data.channel_id, team_id: msg.broadcast.team_id, viewArchivedChannels}});
    +    dispatch({type: ChannelTypes.RECEIVED_CHANNEL_UNARCHIVED, data: {id: msg.data.channel_id, team_id: msg.broadcast.team_id, viewArchivedChannels: true}});
     }
     
     function handlePreferenceChangedEvent(msg) {
    
  • webapp/channels/src/components/admin_console/admin_definition.tsx+0 8 modified
    @@ -2574,14 +2574,6 @@ const AdminDefinition: AdminDefinitionType = {
                                 isHidden: it.not(it.licensedForFeature('LockTeammateNameDisplay')),
                                 isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.USERS_AND_TEAMS)),
                             },
    -                        {
    -                            type: 'bool',
    -                            key: 'TeamSettings.ExperimentalViewArchivedChannels',
    -                            label: defineMessage({id: 'admin.viewArchivedChannelsTitle', defaultMessage: 'Allow users to view archived channels: '}),
    -                            help_text: defineMessage({id: 'admin.viewArchivedChannelsHelpText', defaultMessage: 'When true, allows users to view, share and search for content of channels that have been archived. Users can only view the content in channels of which they were a member before the channel was archived.'}),
    -                            isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.SITE.USERS_AND_TEAMS)),
    -                            isHidden: it.licensedForFeature('Cloud'),
    -                        },
                             {
                                 type: 'bool',
                                 key: 'PrivacySettings.ShowEmailAddress',
    
  • webapp/channels/src/components/browse_channels/browse_channels.test.tsx+0 1 modified
    @@ -120,7 +120,6 @@ describe('components/BrowseChannels', () => {
             teamId: 'team_1',
             teamName: 'team_name',
             channelsRequestStarted: false,
    -        canShowArchivedChannels: true,
             shouldHideJoinedChannels: false,
             myChannelMemberships: {
                 'channel-id-3': TestHelper.getChannelMembershipMock({
    
  • webapp/channels/src/components/browse_channels/browse_channels.tsx+1 6 modified
    @@ -63,7 +63,6 @@ export type Props = {
         teamId: string;
         teamName?: string;
         channelsRequestStarted?: boolean;
    -    canShowArchivedChannels?: boolean;
         myChannelMemberships: RelationOneToOne<Channel, ChannelMembership>;
         shouldHideJoinedChannels: boolean;
         rhsState?: RhsState;
    @@ -110,12 +109,9 @@ export default class BrowseChannels extends React.PureComponent<Props, State> {
     
             const promises = [
                 this.props.actions.getChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2),
    +            this.props.actions.getArchivedChannels(this.props.teamId, 0, CHANNELS_CHUNK_SIZE * 2),
             ];
     
    -        if (this.props.canShowArchivedChannels) {
    -            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) : [];
    @@ -351,7 +347,6 @@ export default class BrowseChannels extends React.PureComponent<Props, State> {
                         loading={search ? searching : channelsRequestStarted}
                         changeFilter={this.changeFilter}
                         filter={this.state.filter}
    -                    canShowArchivedChannels={this.props.canShowArchivedChannels}
                         myChannelMemberships={this.props.myChannelMemberships}
                         closeModal={this.props.actions.closeModal}
                         hideJoinedChannelsPreference={this.handleShowJoinedChannelsPreference}
    
  • webapp/channels/src/components/browse_channels/index.ts+0 2 modified
    @@ -11,7 +11,6 @@ import {getChannels, getArchivedChannels, joinChannel, getChannelsMemberCount, s
     import {RequestStatus} from 'mattermost-redux/constants';
     import {createSelector} from 'mattermost-redux/selectors/create_selector';
     import {getChannelsInCurrentTeam, getMyChannelMemberships, getChannelsMemberCount as getChannelsMemberCountSelector} from 'mattermost-redux/selectors/entities/channels';
    -import {getConfig} from 'mattermost-redux/selectors/entities/general';
     import {getCurrentTeam, getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
     import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
     
    @@ -57,7 +56,6 @@ function mapStateToProps(state: GlobalState) {
             teamId: getCurrentTeamId(state),
             teamName: team?.name,
             channelsRequestStarted: state.requests.channels.getChannels.status === RequestStatus.STARTED,
    -        canShowArchivedChannels: (getConfig(state).ExperimentalViewArchivedChannels === 'true'),
             myChannelMemberships: getMyChannelMemberships(state) || {},
             shouldHideJoinedChannels: getGlobalItem(state) === 'true',
             rhsState: getRhsState(state),
    
  • webapp/channels/src/components/browse_channels/__snapshots__/browse_channels.test.tsx.snap+0 1 modified
    @@ -40,7 +40,6 @@ exports[`components/BrowseChannels should match snapshot and state 1`] = `
       onExited={[Function]}
     >
       <injectIntl(SearchableChannelList)
    -    canShowArchivedChannels={true}
         changeFilter={[Function]}
         channels={
           Array [
    
  • webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.test.tsx+0 1 modified
    @@ -97,7 +97,6 @@ describe('components/ChannelHeaderMenu/MenuItems/ArchiveChannel', () => {
                 dialogType: DeleteChannelModal,
                 dialogProps: {
                     channel,
    -                penultimateViewedChannelName: 'current_channel_id',
                 },
             });
         });
    
  • webapp/channels/src/components/channel_header_menu/menu_items/archive_channel.tsx+1 7 modified
    @@ -3,15 +3,12 @@
     
     import React, {memo} from 'react';
     import {FormattedMessage} from 'react-intl';
    -import {useDispatch, useSelector} from 'react-redux';
    +import {useDispatch} from 'react-redux';
     
     import {ArchiveOutlineIcon} from '@mattermost/compass-icons/components';
     import type {Channel} from '@mattermost/types/channels';
     
    -import {getRedirectChannelNameForCurrentTeam} from 'mattermost-redux/selectors/entities/channels';
    -
     import {openModal} from 'actions/views/modals';
    -import {getPenultimateViewedChannelName} from 'selectors/local_storage';
     
     import DeleteChannelModal from 'components/delete_channel_modal';
     import * as Menu from 'components/menu';
    @@ -26,8 +23,6 @@ const ArchiveChannel = ({
         channel,
     }: Props) => {
         const dispatch = useDispatch();
    -    const redirectChannelName = useSelector(getRedirectChannelNameForCurrentTeam);
    -    const penultimateViewedChannelName = useSelector(getPenultimateViewedChannelName) || redirectChannelName;
     
         const handleArchiveChannel = () => {
             dispatch(
    @@ -36,7 +31,6 @@ const ArchiveChannel = ({
                     dialogType: DeleteChannelModal,
                     dialogProps: {
                         channel,
    -                    penultimateViewedChannelName,
                     },
                 }),
             );
    
  • webapp/channels/src/components/channel_settings_modal/channel_settings_archive_tab.test.tsx+0 12 modified
    @@ -5,7 +5,6 @@ import {screen, waitFor} from '@testing-library/react';
     import userEvent from '@testing-library/user-event';
     import React from 'react';
     
    -import type {ClientConfig} from '@mattermost/types/config';
     import type {Team} from '@mattermost/types/teams';
     
     import * as teams from 'mattermost-redux/selectors/entities/teams';
    @@ -26,11 +25,6 @@ jest.mock('utils/browser_history', () => ({
         getHistory: jest.fn(),
     }));
     
    -jest.mock('mattermost-redux/selectors/entities/general', () => ({
    -    ...jest.requireActual('mattermost-redux/selectors/entities/general') as typeof import('mattermost-redux/selectors/entities/general'),
    -    getConfig: () => mockConfig,
    -}));
    -
     // Mock the roles selector which is a dependency for other selectors
     jest.mock('mattermost-redux/selectors/entities/roles', () => ({
         haveITeamPermission: jest.fn().mockReturnValue(true),
    @@ -52,17 +46,11 @@ const baseProps = {
         onHide: jest.fn(),
     };
     
    -let mockConfig: Partial<ClientConfig>;
    -
     describe('ChannelSettingsArchiveTab', () => {
         const {getHistory} = require('utils/browser_history');
         beforeEach(() => {
             jest.clearAllMocks();
     
    -        mockConfig = {
    -            ExperimentalViewArchivedChannels: 'false',
    -        };
    -
             jest.spyOn(teams, 'getCurrentTeam').mockReturnValue({
                 id: 'team1',
                 name: 'team-name',
    
  • webapp/channels/src/components/channel_settings_modal/channel_settings_archive_tab.tsx+2 12 modified
    @@ -3,18 +3,14 @@
     
     import React, {useState, useCallback} from 'react';
     import {FormattedMessage, useIntl} from 'react-intl';
    -import {useDispatch, useSelector} from 'react-redux';
    +import {useDispatch} from 'react-redux';
     
     import type {Channel} from '@mattermost/types/channels';
     
    -import {getConfig} from 'mattermost-redux/selectors/entities/general';
    -
     import {deleteChannel} from 'actions/views/channel';
     
     import ConfirmationModal from 'components/confirm_modal';
     
    -import type {GlobalState} from 'types/store';
    -
     type ChannelSettingsArchiveTabProps = {
         channel: Channel;
         onHide: () => void;
    @@ -27,9 +23,6 @@ function ChannelSettingsArchiveTab({
         const {formatMessage} = useIntl();
         const dispatch = useDispatch();
     
    -    // Redux selector
    -    const canViewArchivedChannels = useSelector((state: GlobalState) => getConfig(state).ExperimentalViewArchivedChannels === 'true');
    -
         const [showArchiveConfirmModal, setShowArchiveConfirmModal] = useState(false);
     
         const handleArchiveChannel = useCallback(() => {
    @@ -72,10 +65,7 @@ function ChannelSettingsArchiveTab({
                             <div>
                                 <p>
                                     <FormattedMessage
    -                                    id={canViewArchivedChannels ?
    -                                        'deleteChannelModal.canViewArchivedChannelsWarning' :
    -                                        'deleteChannelModal.cannotViewArchivedChannelsWarning'
    -                                    }
    +                                    id='deleteChannelModal.canViewArchivedChannelsWarning'
                                         defaultMessage="Archiving a channel removes it from the user interface, but doesn't permanently delete the channel. New messages can't be posted to archived channels."
                                     />
                                 </p>
    
  • webapp/channels/src/components/channel_view/channel_view.test.tsx+0 1 modified
    @@ -20,7 +20,6 @@ describe('components/channel_view', () => {
             enableOnboardingFlow: true,
             teamUrl: '/team',
             channelIsArchived: false,
    -        viewArchivedChannels: false,
             isCloud: false,
             goToLastViewedChannel: jest.fn(),
             isFirstAdmin: false,
    
  • webapp/channels/src/components/channel_view/channel_view.tsx+0 5 modified
    @@ -100,11 +100,6 @@ export default class ChannelView extends React.PureComponent<Props, State> {
             if (prevProps.channelId !== this.props.channelId && this.props.enableWebSocketEventScope) {
                 WebSocketClient.updateActiveChannel(this.props.channelId);
             }
    -        if (prevProps.channelId !== this.props.channelId || prevProps.channelIsArchived !== this.props.channelIsArchived) {
    -            if (this.props.channelIsArchived && !this.props.viewArchivedChannels) {
    -                this.props.goToLastViewedChannel();
    -            }
    -        }
         }
     
         render() {
    
  • webapp/channels/src/components/channel_view/index.ts+0 2 modified
    @@ -35,7 +35,6 @@ function mapStateToProps(state: GlobalState) {
     
         const config = getConfig(state);
     
    -    const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
         const enableOnboardingFlow = config.EnableOnboardingFlow === 'true';
         const enableWebSocketEventScope = config.FeatureFlagWebSocketEventScope === 'true';
     
    @@ -46,7 +45,6 @@ function mapStateToProps(state: GlobalState) {
             deactivatedChannel: channel ? isDeactivatedDirectChannel(state, channel.id) : false,
             enableOnboardingFlow,
             channelIsArchived: channel ? channel.delete_at !== 0 : false,
    -        viewArchivedChannels,
             isCloud: getLicense(state).Cloud === 'true',
             teamUrl: getCurrentRelativeTeamUrl(state),
             isFirstAdmin: isFirstAdmin(state),
    
  • webapp/channels/src/components/channel_view/__snapshots__/channel_view.test.tsx.snap+0 3 modified
    @@ -29,7 +29,6 @@ exports[`components/channel_view Should match snapshot if channel is archived 1`
         }
         missingChannelRole={false}
         teamUrl="/team"
    -    viewArchivedChannels={false}
       />
       <ChannelBanner
         channelId="channelId"
    @@ -97,7 +96,6 @@ exports[`components/channel_view Should match snapshot if channel is deactivated
         }
         missingChannelRole={false}
         teamUrl="/team"
    -    viewArchivedChannels={false}
       />
       <ChannelBanner
         channelId="channelId"
    @@ -164,7 +162,6 @@ exports[`components/channel_view Should match snapshot with base props 1`] = `
         }
         missingChannelRole={false}
         teamUrl="/team"
    -    viewArchivedChannels={false}
       />
       <ChannelBanner
         channelId="channelId"
    
  • webapp/channels/src/components/delete_channel_modal/delete_channel_modal.test.tsx+0 9 modified
    @@ -10,8 +10,6 @@ import type {Channel, ChannelType} from '@mattermost/types/channels';
     import DeleteChannelModal from 'components/delete_channel_modal/delete_channel_modal';
     import type {Props} from 'components/delete_channel_modal/delete_channel_modal';
     
    -import {getHistory} from 'utils/browser_history';
    -
     describe('components/delete_channel_modal', () => {
         const channel: Channel = {
             id: 'owsyt8n43jfxjpzh9np93mx1wa',
    @@ -31,20 +29,14 @@ describe('components/delete_channel_modal', () => {
             group_constrained: false,
         };
     
    -    const currentTeamDetails = {
    -        name: 'mattermostDev',
    -    };
    -
         const baseProps: Props = {
             channel,
    -        currentTeamDetails,
             actions: {
                 deleteChannel: jest.fn(() => {
                     return {data: true};
                 }),
             },
             onExited: jest.fn(),
    -        penultimateViewedChannelName: 'my-prev-channel',
         };
     
         test('should match snapshot for delete_channel_modal', () => {
    @@ -76,7 +68,6 @@ describe('components/delete_channel_modal', () => {
     
             expect(actions.deleteChannel).toHaveBeenCalledTimes(1);
             expect(actions.deleteChannel).toHaveBeenCalledWith(props.channel.id);
    -        expect(getHistory().push).toHaveBeenCalledWith('/mattermostDev/channels/my-prev-channel');
             expect(wrapper.state('show')).toEqual(false);
         });
     
    
  • webapp/channels/src/components/delete_channel_modal/delete_channel_modal.tsx+16 51 modified
    @@ -7,15 +7,11 @@ import {FormattedMessage} from 'react-intl';
     
     import type {Channel} from '@mattermost/types/channels';
     
    -import {getHistory} from 'utils/browser_history';
     import Constants from 'utils/constants';
     
     export type Props = {
         onExited: () => void;
         channel: Channel;
    -    currentTeamDetails?: {name: string};
    -    canViewArchivedChannels?: boolean;
    -    penultimateViewedChannelName: string;
         actions: {
             deleteChannel: (channelId: string) => void;
         };
    @@ -35,12 +31,6 @@ export default class DeleteChannelModal extends React.PureComponent<Props, State
             if (this.props.channel.id.length !== Constants.CHANNEL_ID_LENGTH) {
                 return;
             }
    -        if (!this.props.canViewArchivedChannels) {
    -            const {penultimateViewedChannelName} = this.props;
    -            if (this.props.currentTeamDetails) {
    -                getHistory().push('/' + this.props.currentTeamDetails.name + '/channels/' + penultimateViewedChannelName);
    -            }
    -        }
             this.props.actions.deleteChannel(this.props.channel.id);
             this.onHide();
         };
    @@ -50,7 +40,6 @@ export default class DeleteChannelModal extends React.PureComponent<Props, State
         };
     
         render() {
    -        const {canViewArchivedChannels} = this.props;
             return (
                 <Modal
                     dialogClassName='a11y__modal'
    @@ -74,46 +63,22 @@ export default class DeleteChannelModal extends React.PureComponent<Props, State
                     </Modal.Header>
                     <Modal.Body>
                         <div className='alert alert-danger'>
    -                        {!canViewArchivedChannels &&
    -                            <>
    -                                <p>
    -                                    <FormattedMessage
    -                                        id='deleteChannelModal.cannotViewArchivedChannelsWarning'
    -                                        defaultMessage='This will archive the channel from the team and remove it from the user interface. Archived channels can be unarchived if needed again.'
    -                                    />
    -                                </p>
    -                                <p>
    -                                    <FormattedMessage
    -                                        id='deleteChannelModal.confirmArchive'
    -                                        defaultMessage='Are you sure you wish to archive the <strong>{display_name}</strong> channel?'
    -                                        values={{
    -                                            display_name: this.props.channel.display_name,
    -                                            strong: (chunks: string) => <strong>{chunks}</strong>,
    -                                        }}
    -                                    />
    -                                </p>
    -                            </>
    -                        }
    -                        {canViewArchivedChannels &&
    -                            <>
    -                                <p>
    -                                    <FormattedMessage
    -                                        id='deleteChannelModal.canViewArchivedChannelsWarning'
    -                                        defaultMessage='This will archive the channel from the team. Channel contents will still be accessible by channel members.'
    -                                    />
    -                                </p>
    -                                <p>
    -                                    <FormattedMessage
    -                                        id='deleteChannelModal.confirmArchive'
    -                                        defaultMessage='Are you sure you wish to archive the <strong>{display_name}</strong> channel?'
    -                                        values={{
    -                                            display_name: this.props.channel.display_name,
    -                                            strong: (chunks: string) => <strong>{chunks}</strong>,
    -                                        }}
    -                                    />
    -                                </p>
    -                            </>
    -                        }
    +                        <p>
    +                            <FormattedMessage
    +                                id='deleteChannelModal.canViewArchivedChannelsWarning'
    +                                defaultMessage='This will archive the channel from the team. Channel contents will still be accessible by channel members.'
    +                            />
    +                        </p>
    +                        <p>
    +                            <FormattedMessage
    +                                id='deleteChannelModal.confirmArchive'
    +                                defaultMessage='Are you sure you wish to archive the <strong>{display_name}</strong> channel?'
    +                                values={{
    +                                    display_name: this.props.channel.display_name,
    +                                    strong: (chunks: string) => <strong>{chunks}</strong>,
    +                                }}
    +                            />
    +                        </p>
                         </div>
                     </Modal.Body>
                     <Modal.Footer>
    
  • webapp/channels/src/components/delete_channel_modal/index.ts+0 4 modified
    @@ -7,18 +7,14 @@ import type {Dispatch} from 'redux';
     
     import type {GlobalState} from '@mattermost/types/store';
     
    -import {getConfig} from 'mattermost-redux/selectors/entities/general';
     import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
     
     import {deleteChannel} from 'actions/views/channel';
     
     import DeleteChannelModal from './delete_channel_modal';
     
     function mapStateToProps(state: GlobalState) {
    -    const config = getConfig(state);
    -
         return {
    -        canViewArchivedChannels: config.ExperimentalViewArchivedChannels === 'true',
             currentTeamDetails: getCurrentTeam(state),
         };
     }
    
  • webapp/channels/src/components/delete_channel_modal/__snapshots__/delete_channel_modal.test.tsx.snap+2 2 modified
    @@ -56,8 +56,8 @@ exports[`components/delete_channel_modal should match snapshot for delete_channe
         >
           <p>
             <MemoizedFormattedMessage
    -          defaultMessage="This will archive the channel from the team and remove it from the user interface. Archived channels can be unarchived if needed again."
    -          id="deleteChannelModal.cannotViewArchivedChannelsWarning"
    +          defaultMessage="This will archive the channel from the team. Channel contents will still be accessible by channel members."
    +          id="deleteChannelModal.canViewArchivedChannelsWarning"
             />
           </p>
           <p>
    
  • webapp/channels/src/components/searchable_channel_list.tsx+17 20 modified
    @@ -43,7 +43,6 @@ interface Props extends WrappedComponentProps {
         closeModal: (modalId: string) => void;
         hideJoinedChannelsPreference: (shouldHideJoinedChannels: boolean) => void;
         rememberHideJoinedChannelsChecked: boolean;
    -    canShowArchivedChannels?: boolean;
         loading?: boolean;
         channelsMemberCount?: Record<string, number>;
     }
    @@ -496,25 +495,23 @@ export class SearchableChannelList extends React.PureComponent<Props, State> {
                 />,
             ];
     
    -        if (this.props.canShowArchivedChannels) {
    -            channelDropdownItems.push(
    -                <Menu.Separator key='channelsMoreDropdownSeparator'/>,
    -                <Menu.Item
    -                    key='channelsMoreDropdownArchived'
    -                    id='channelsMoreDropdownArchived'
    -                    onClick={() => this.filterChange(Filter.Archived)}
    -                    leadingElement={<ArchiveOutlineIcon size={16}/>}
    -                    labels={
    -                        <FormattedMessage
    -                            id='suggestion.archive'
    -                            defaultMessage='Archived channels'
    -                        />
    -                    }
    -                    trailingElements={this.props.filter === Filter.Archived ? checkIcon : null}
    -                    aria-label={this.props.intl.formatMessage({id: 'suggestion.archive', defaultMessage: 'Archived channels'})}
    -                />,
    -            );
    -        }
    +        channelDropdownItems.push(
    +            <Menu.Separator key='channelsMoreDropdownSeparator'/>,
    +            <Menu.Item
    +                key='channelsMoreDropdownArchived'
    +                id='channelsMoreDropdownArchived'
    +                onClick={() => this.filterChange(Filter.Archived)}
    +                leadingElement={<ArchiveOutlineIcon size={16}/>}
    +                labels={
    +                    <FormattedMessage
    +                        id='suggestion.archive'
    +                        defaultMessage='Archived channels'
    +                    />
    +                }
    +                trailingElements={this.props.filter === Filter.Archived ? checkIcon : null}
    +                aria-label={this.props.intl.formatMessage({id: 'suggestion.archive', defaultMessage: 'Archived channels'})}
    +            />,
    +        );
             const menuButton = (
                 <>
                     {this.getFilterLabel()}
    
  • webapp/channels/src/components/search_results/index.tsx+1 12 modified
    @@ -6,9 +6,7 @@ import {connect} from 'react-redux';
     import type {FileSearchResultItem} from '@mattermost/types/files';
     import type {Post} from '@mattermost/types/posts';
     
    -import {getChannel} from 'mattermost-redux/selectors/entities/channels';
     import {getSearchFilesResults} from 'mattermost-redux/selectors/entities/files';
    -import {getConfig} from 'mattermost-redux/selectors/entities/general';
     import {getSearchMatches, getSearchResults} from 'mattermost-redux/selectors/entities/posts';
     import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
     import {makeAddDateSeparatorsForSearchResults} from 'mattermost-redux/utils/post_list';
    @@ -36,10 +34,6 @@ function makeMapStateToProps() {
         const addDateSeparatorsForSearchResults = makeAddDateSeparatorsForSearchResults();
     
         return function mapStateToProps(state: GlobalState, ownProps: OwnProps) {
    -        const config = getConfig(state);
    -
    -        const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
    -
             const newResults = getSearchResults(state);
     
             // Cache posts and channels
    @@ -56,7 +50,7 @@ function makeMapStateToProps() {
                 });
     
                 if (ownProps.isPinnedPosts) {
    -                results = results.sort((postA: Post|FileSearchResultItem, postB: Post|FileSearchResultItem) => postB.create_at - postA.create_at);
    +                results = results.sort((postA: Post | FileSearchResultItem, postB: Post | FileSearchResultItem) => postB.create_at - postA.create_at);
                 }
             }
     
    @@ -72,11 +66,6 @@ function makeMapStateToProps() {
                         return;
                     }
     
    -                const channel = getChannel(state, file.channel_id);
    -                if (channel && channel.delete_at !== 0 && !viewArchivedChannels) {
    -                    return;
    -                }
    -
                     files.push(file);
                 });
             }
    
  • webapp/channels/src/components/__snapshots__/searchable_channel_list.test.tsx.snap+21 0 modified
    @@ -134,6 +134,27 @@ exports[`components/SearchableChannelList should match init snapshot 1`] = `
               onClick={[Function]}
               trailingElements={null}
             />
    +        <MenuItemSeparator
    +          key="channelsMoreDropdownSeparator"
    +        />
    +        <MenuItem
    +          aria-label="Archived channels"
    +          id="channelsMoreDropdownArchived"
    +          key="channelsMoreDropdownArchived"
    +          labels={
    +            <Memo(MemoizedFormattedMessage)
    +              defaultMessage="Archived channels"
    +              id="suggestion.archive"
    +            />
    +          }
    +          leadingElement={
    +            <ArchiveOutlineIcon
    +              size={16}
    +            />
    +          }
    +          onClick={[Function]}
    +          trailingElements={null}
    +        />
           </Menu>
           <div
             aria-checked={false}
    
  • webapp/channels/src/components/suggestion/search_channel_with_permissions_provider.tsx+1 8 modified
    @@ -12,7 +12,6 @@ import {
         getChannelsInCurrentTeam,
     } from 'mattermost-redux/selectors/entities/channels';
     import {getMyChannelMemberships} from 'mattermost-redux/selectors/entities/common';
    -import {getConfig} from 'mattermost-redux/selectors/entities/general';
     import {getCurrentUserLocale} from 'mattermost-redux/selectors/entities/i18n';
     import {haveIChannelPermission} from 'mattermost-redux/selectors/entities/roles';
     import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
    @@ -230,22 +229,16 @@ export default class SearchChannelWithPermissionsProvider extends Provider {
     
             const channelFilter = this.makeChannelSearchFilter(channelPrefix);
     
    -        const config = getConfig(state);
    -        const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
    -
             for (const channel of allChannels) {
                 if (completedChannels[channel.id]) {
                     continue;
                 }
     
                 if (channelFilter(channel)) {
                     const newChannel = Object.assign({}, channel);
    -                const channelIsArchived = channel.delete_at !== 0;
     
                     const wrappedChannel = {channel: newChannel, name: newChannel.name, deactivated: false, type: newChannel.type};
    -                if (!viewArchivedChannels && channelIsArchived) {
    -                    continue;
    -                } else if (!members[channel.id]) {
    +                if (!members[channel.id]) {
                         continue;
                     } else if (channel.type !== Constants.OPEN_CHANNEL && channel.type !== Constants.PRIVATE_CHANNEL) {
                         continue;
    
  • webapp/channels/src/components/suggestion/switch_channel_provider.tsx+1 5 modified
    @@ -709,8 +709,6 @@ export default class SwitchChannelProvider extends Provider {
             const channelFilter = makeChannelSearchFilter(this.store.getState(), channelPrefix);
     
             const state = this.store.getState();
    -        const config = getConfig(state);
    -        const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
             const allUnreadChannelIds = getAllTeamsUnreadChannelIds(state);
             const allUnreadChannelIdsSet = new Set(allUnreadChannelIds);
             const currentUserId = getCurrentUserId(state);
    @@ -730,9 +728,7 @@ export default class SwitchChannelProvider extends Provider {
                         continue;
                     }
     
    -                if (!viewArchivedChannels && channelIsArchived) {
    -                    continue;
    -                } else if (channelIsArchived && members[channel.id]) {
    +                if (channelIsArchived && members[channel.id]) {
                         wrappedChannel.type = Constants.ARCHIVED_CHANNEL;
                     } else if (newChannel.type === Constants.OPEN_CHANNEL) {
                         wrappedChannel.type = Constants.MENTION_PUBLIC_CHANNELS;
    
  • webapp/channels/src/i18n/en.json+0 3 modified
    @@ -3144,8 +3144,6 @@
       "admin.userMultiSelector.loading": "Loading users",
       "admin.userMultiSelector.noUsers": "No users found",
       "admin.userMultiSelector.placeholder": "Start typing to search for users...",
    -  "admin.viewArchivedChannelsHelpText": "When true, allows users to view, share and search for content of channels that have been archived. Users can only view the content in channels of which they were a member before the channel was archived.",
    -  "admin.viewArchivedChannelsTitle": "Allow users to view archived channels:",
       "admin.webserverModeDisabled": "Disabled",
       "admin.webserverModeDisabledDescription": "The Mattermost server will not serve static files.",
       "admin.webserverModeGzip": "gzip",
    @@ -4001,7 +3999,6 @@
       "delete_post.shared_channel_warning.title": "Shared Channel",
       "delete_post.warning": "This message has {count, number} {count, plural, one {comment} other {comments}} on it.",
       "delete_success_modal.button_text": "Go to mattermost.com",
    -  "deleteChannelModal.cannotViewArchivedChannelsWarning": "This will archive the channel from the team and remove it from the user interface. Archived channels can be unarchived if needed again.",
       "deleteChannelModal.canViewArchivedChannelsWarning": "This will archive the channel from the team. Channel contents will still be accessible by channel members.",
       "deleteChannelModal.confirmArchive": "Are you sure you wish to archive the <b>{display_name}</b> channel?",
       "demote_to_user_modal.demote": "Demote",
    
  • webapp/channels/src/packages/mattermost-redux/src/actions/channels.ts+2 23 modified
    @@ -27,15 +27,11 @@ import {MarkUnread} from 'mattermost-redux/constants/channels';
     import {getCategoryInTeamByType} from 'mattermost-redux/selectors/entities/channel_categories';
     import {
         getChannel as getChannelSelector,
    -    getChannelsNameMapInTeam,
         getMyChannelMember as getMyChannelMemberSelector,
    -    getRedirectChannelNameForTeam,
         isManuallyUnread,
     } from 'mattermost-redux/selectors/entities/channels';
    -import {getConfig} from 'mattermost-redux/selectors/entities/general';
     import {getCurrentTeamId} from 'mattermost-redux/selectors/entities/teams';
     import type {GetStateFunc, ActionFunc, ActionFuncAsync} from 'mattermost-redux/types/actions';
    -import {getChannelByName} from 'mattermost-redux/utils/channel_utils';
     import {DelayedDataLoader} from 'mattermost-redux/utils/data_loader';
     
     import {addChannelToInitialCategory, addChannelToCategory} from './channel_categories';
    @@ -644,9 +640,6 @@ export function joinChannel(userId: string, teamId: string, channelId: string, c
     
     export function deleteChannel(channelId: string): ActionFuncAsync {
         return async (dispatch, getState) => {
    -        let state = getState();
    -        const viewArchivedChannels = state.entities.general.config.ExperimentalViewArchivedChannels === 'true';
    -
             try {
                 await Client4.deleteChannel(channelId);
             } catch (error) {
    @@ -655,18 +648,7 @@ export function deleteChannel(channelId: string): ActionFuncAsync {
                 return {error};
             }
     
    -        state = getState();
    -        const {currentChannelId} = state.entities.channels;
    -        if (channelId === currentChannelId && !viewArchivedChannels) {
    -            const teamId = getCurrentTeamId(state);
    -            const channelsInTeam = getChannelsNameMapInTeam(state, teamId);
    -            const channel = getChannelByName(channelsInTeam, getRedirectChannelNameForTeam(state, teamId));
    -            if (channel && channel.id) {
    -                dispatch({type: ChannelTypes.SELECT_CHANNEL, data: channel.id});
    -            }
    -        }
    -
    -        dispatch({type: ChannelTypes.DELETE_CHANNEL_SUCCESS, data: {id: channelId, viewArchivedChannels}});
    +        dispatch({type: ChannelTypes.DELETE_CHANNEL_SUCCESS, data: {id: channelId}});
     
             return {data: true};
         };
    @@ -682,10 +664,7 @@ export function unarchiveChannel(channelId: string): ActionFuncAsync {
                 return {error};
             }
     
    -        const state = getState();
    -        const config = getConfig(state);
    -        const viewArchivedChannels = config.ExperimentalViewArchivedChannels === 'true';
    -        dispatch({type: ChannelTypes.UNARCHIVED_CHANNEL_SUCCESS, data: {id: channelId, viewArchivedChannels}});
    +        dispatch({type: ChannelTypes.UNARCHIVED_CHANNEL_SUCCESS, data: {id: channelId}});
     
             return {data: true};
         };
    
  • webapp/channels/src/packages/mattermost-redux/src/reducers/entities/posts.test.ts+229 271 modified
    @@ -466,77 +466,72 @@ describe('posts', () => {
             });
         });
     
    -    for (const actionType of [
    -        ChannelTypes.RECEIVED_CHANNEL_DELETED,
    -        ChannelTypes.DELETE_CHANNEL_SUCCESS,
    -        ChannelTypes.LEAVE_CHANNEL,
    -    ]) {
    -        describe(`when a channel is deleted (${actionType})`, () => {
    -            it('should remove any posts in that channel', () => {
    -                const state = deepFreeze({
    -                    post1: {id: 'post1', channel_id: 'channel1'},
    -                    post2: {id: 'post2', channel_id: 'channel1'},
    -                    post3: {id: 'post3', channel_id: 'channel2'},
    -                });
    -
    -                const nextState = reducers.handlePosts(state, {
    -                    type: actionType,
    -                    data: {
    -                        id: 'channel1',
    -                        viewArchivedChannels: false,
    -                    },
    -                });
    +    describe('when a channel is left (LEAVE_CHANNEL)', () => {
    +        it('should remove any posts in that channel', () => {
    +            const state = deepFreeze({
    +                post1: {id: 'post1', channel_id: 'channel1'},
    +                post2: {id: 'post2', channel_id: 'channel1'},
    +                post3: {id: 'post3', channel_id: 'channel2'},
    +            });
     
    -                expect(nextState).not.toBe(state);
    -                expect(nextState.post3).toBe(state.post3);
    -                expect(nextState).toEqual({
    -                    post3: {id: 'post3', channel_id: 'channel2'},
    -                });
    +            const nextState = reducers.handlePosts(state, {
    +                type: ChannelTypes.LEAVE_CHANNEL,
    +                data: {
    +                    id: 'channel1',
    +                    viewArchivedChannels: false,
    +                },
                 });
     
    -            it('should do nothing if no posts in that channel are loaded', () => {
    -                const state = deepFreeze({
    -                    post1: {id: 'post1', channel_id: 'channel1'},
    -                    post2: {id: 'post2', channel_id: 'channel1'},
    -                    post3: {id: 'post3', channel_id: 'channel2'},
    -                });
    +            expect(nextState).not.toBe(state);
    +            expect(nextState.post3).toBe(state.post3);
    +            expect(nextState).toEqual({
    +                post3: {id: 'post3', channel_id: 'channel2'},
    +            });
    +        });
     
    -                const nextState = reducers.handlePosts(state, {
    -                    type: actionType,
    -                    data: {
    -                        id: 'channel3',
    -                        viewArchivedChannels: false,
    -                    },
    -                });
    +        it('should do nothing if no posts in that channel are loaded', () => {
    +            const state = deepFreeze({
    +                post1: {id: 'post1', channel_id: 'channel1'},
    +                post2: {id: 'post2', channel_id: 'channel1'},
    +                post3: {id: 'post3', channel_id: 'channel2'},
    +            });
     
    -                expect(nextState).toBe(state);
    -                expect(nextState.post1).toBe(state.post1);
    -                expect(nextState.post2).toBe(state.post2);
    -                expect(nextState.post3).toBe(state.post3);
    +            const nextState = reducers.handlePosts(state, {
    +                type: ChannelTypes.LEAVE_CHANNEL,
    +                data: {
    +                    id: 'channel3',
    +                    viewArchivedChannels: false,
    +                },
                 });
     
    -            it('should not remove any posts with viewArchivedChannels enabled', () => {
    -                const state = deepFreeze({
    -                    post1: {id: 'post1', channel_id: 'channel1'},
    -                    post2: {id: 'post2', channel_id: 'channel1'},
    -                    post3: {id: 'post3', channel_id: 'channel2'},
    -                });
    +            expect(nextState).toBe(state);
    +            expect(nextState.post1).toBe(state.post1);
    +            expect(nextState.post2).toBe(state.post2);
    +            expect(nextState.post3).toBe(state.post3);
    +        });
     
    -                const nextState = reducers.handlePosts(state, {
    -                    type: actionType,
    -                    data: {
    -                        id: 'channel1',
    -                        viewArchivedChannels: true,
    -                    },
    -                });
    +        it('should remove posts unconditionally (ignoring viewArchivedChannels flag)', () => {
    +            const state = deepFreeze({
    +                post1: {id: 'post1', channel_id: 'channel1'},
    +                post2: {id: 'post2', channel_id: 'channel1'},
    +                post3: {id: 'post3', channel_id: 'channel2'},
    +            });
     
    -                expect(nextState).toBe(state);
    -                expect(nextState.post1).toBe(state.post1);
    -                expect(nextState.post2).toBe(state.post2);
    -                expect(nextState.post3).toBe(state.post3);
    +            const nextState = reducers.handlePosts(state, {
    +                type: ChannelTypes.LEAVE_CHANNEL,
    +                data: {
    +                    id: 'channel1',
    +                    viewArchivedChannels: true,
    +                },
    +            });
    +
    +            expect(nextState).not.toBe(state);
    +            expect(nextState.post3).toBe(state.post3);
    +            expect(nextState).toEqual({
    +                post3: {id: 'post3', channel_id: 'channel2'},
                 });
             });
    -    }
    +    });
     
         describe(`follow a post/thread (${ThreadTypes.FOLLOW_CHANGED_THREAD})`, () => {
             test.each([[true], [false]])('should set is_following to %s', (following) => {
    @@ -2573,105 +2568,94 @@ describe('postsInChannel', () => {
             });
         });
     
    -    for (const actionType of [
    -        ChannelTypes.RECEIVED_CHANNEL_DELETED,
    -        ChannelTypes.DELETE_CHANNEL_SUCCESS,
    -        ChannelTypes.LEAVE_CHANNEL,
    -    ]) {
    -        describe(`when a channel is deleted (${actionType})`, () => {
    -            it('should remove any posts in that channel', () => {
    -                const state = deepFreeze({
    -                    channel1: [
    -                        {order: ['post1', 'post2', 'post3'], recent: false},
    -                        {order: ['post6', 'post7', 'post8'], recent: false},
    -                    ],
    -                    channel2: [
    -                        {order: ['post4', 'post5'], recent: false},
    -                    ],
    -                });
    +    describe('when a channel is left (LEAVE_CHANNEL)', () => {
    +        it('should remove any posts in that channel', () => {
    +            const state = deepFreeze({
    +                channel1: [
    +                    {order: ['post1', 'post2', 'post3'], recent: false},
    +                    {order: ['post6', 'post7', 'post8'], recent: false},
    +                ],
    +                channel2: [
    +                    {order: ['post4', 'post5'], recent: false},
    +                ],
    +            });
     
    -                const nextState = reducers.postsInChannel(state, {
    -                    type: actionType,
    -                    data: {
    -                        id: 'channel1',
    -                        viewArchivedChannels: false,
    -                    },
    -                }, {}, {});
    +            const nextState = reducers.postsInChannel(state, {
    +                type: ChannelTypes.LEAVE_CHANNEL,
    +                data: {
    +                    id: 'channel1',
    +                    viewArchivedChannels: false,
    +                },
    +            }, {}, {});
     
    -                expect(nextState).not.toBe(state);
    -                expect(nextState.channel2).toBe(state.channel2);
    -                expect(nextState).toEqual({
    -                    channel2: [
    -                        {order: ['post4', 'post5'], recent: false},
    -                    ],
    -                });
    +            expect(nextState).not.toBe(state);
    +            expect(nextState.channel2).toBe(state.channel2);
    +            expect(nextState).toEqual({
    +                channel2: [
    +                    {order: ['post4', 'post5'], recent: false},
    +                ],
                 });
    +        });
     
    -            it('should do nothing if no posts in that channel are loaded', () => {
    -                const state = deepFreeze({
    -                    channel1: [
    -                        {order: ['post1', 'post2', 'post3'], recent: false},
    -                    ],
    -                    channel2: [
    -                        {order: ['post4', 'post5'], recent: false},
    -                    ],
    -                });
    +        it('should do nothing if no posts in that channel are loaded', () => {
    +            const state = deepFreeze({
    +                channel1: [
    +                    {order: ['post1', 'post2', 'post3'], recent: false},
    +                ],
    +                channel2: [
    +                    {order: ['post4', 'post5'], recent: false},
    +                ],
    +            });
     
    -                const nextState = reducers.postsInChannel(state, {
    -                    type: actionType,
    -                    data: {
    -                        id: 'channel3',
    -                        viewArchivedChannels: false,
    -                    },
    -                }, {}, {});
    +            const nextState = reducers.postsInChannel(state, {
    +                type: ChannelTypes.LEAVE_CHANNEL,
    +                data: {
    +                    id: 'channel3',
    +                    viewArchivedChannels: false,
    +                },
    +            }, {}, {});
     
    -                expect(nextState).toBe(state);
    -                expect(nextState.channel1).toBe(state.channel1);
    -                expect(nextState.channel2).toBe(state.channel2);
    -                expect(nextState).toEqual({
    -                    channel1: [
    -                        {order: ['post1', 'post2', 'post3'], recent: false},
    -                    ],
    -                    channel2: [
    -                        {order: ['post4', 'post5'], recent: false},
    -                    ],
    -                });
    +            expect(nextState).toBe(state);
    +            expect(nextState.channel1).toBe(state.channel1);
    +            expect(nextState.channel2).toBe(state.channel2);
    +            expect(nextState).toEqual({
    +                channel1: [
    +                    {order: ['post1', 'post2', 'post3'], recent: false},
    +                ],
    +                channel2: [
    +                    {order: ['post4', 'post5'], recent: false},
    +                ],
                 });
    +        });
     
    -            it('should not remove any posts with viewArchivedChannels enabled', () => {
    -                const state = deepFreeze({
    -                    channel1: [
    -                        {order: ['post1', 'post2', 'post3'], recent: false},
    -                        {order: ['post6', 'post7', 'post8'], recent: false},
    -                    ],
    -                    channel2: [
    -                        {order: ['post4', 'post5'], recent: false},
    -                    ],
    -                });
    +        it('should remove posts unconditionally (ignoring viewArchivedChannels flag)', () => {
    +            const state = deepFreeze({
    +                channel1: [
    +                    {order: ['post1', 'post2', 'post3'], recent: false},
    +                    {order: ['post6', 'post7', 'post8'], recent: false},
    +                ],
    +                channel2: [
    +                    {order: ['post4', 'post5'], recent: false},
    +                ],
    +            });
     
    -                const nextState = reducers.postsInChannel(state, {
    -                    type: actionType,
    -                    data: {
    -                        id: 'channel1',
    -                        viewArchivedChannels: true,
    -                    },
    -                }, {}, {});
    +            const nextState = reducers.postsInChannel(state, {
    +                type: ChannelTypes.LEAVE_CHANNEL,
    +                data: {
    +                    id: 'channel1',
    +                    viewArchivedChannels: true,
    +                },
    +            }, {}, {});
     
    -                expect(nextState).toBe(state);
    -                expect(nextState.channel1).toBe(state.channel1);
    -                expect(nextState.channel2).toBe(state.channel2);
    -                expect(nextState).toEqual({
    -                    channel1: [
    -                        {order: ['post1', 'post2', 'post3'], recent: false},
    -                        {order: ['post6', 'post7', 'post8'], recent: false},
    -                    ],
    -                    channel2: [
    -                        {order: ['post4', 'post5'], recent: false},
    -                    ],
    -                });
    +            expect(nextState).not.toBe(state);
    +            expect(nextState.channel2).toBe(state.channel2);
    +            expect(nextState).toEqual({
    +                channel2: [
    +                    {order: ['post4', 'post5'], recent: false},
    +                ],
                 });
             });
    -    }
    +    });
     });
     
     describe('mergePostBlocks', () => {
    @@ -3379,119 +3363,113 @@ describe('postsInThread', () => {
             });
         });
     
    -    for (const actionType of [
    -        ChannelTypes.RECEIVED_CHANNEL_DELETED,
    -        ChannelTypes.DELETE_CHANNEL_SUCCESS,
    -        ChannelTypes.LEAVE_CHANNEL,
    -    ]) {
    -        describe(`when a channel is deleted (${actionType})`, () => {
    -            it('should remove any threads in that channel', () => {
    -                const state = deepFreeze({
    -                    root1: ['comment1', 'comment2'],
    -                    root2: ['comment3'],
    -                    root3: ['comment4'],
    -                });
    +    describe('when a channel is left (LEAVE_CHANNEL)', () => {
    +        it('should remove any threads in that channel', () => {
    +            const state = deepFreeze({
    +                root1: ['comment1', 'comment2'],
    +                root2: ['comment3'],
    +                root3: ['comment4'],
    +            });
     
    -                const prevPosts = toPostsRecord({
    -                    root1: {id: 'root1', channel_id: 'channel1'},
    -                    comment1: {id: 'comment1', channel_id: 'channel1', root_id: 'root1'},
    -                    comment2: {id: 'comment2', channel_id: 'channel1', root_id: 'root1'},
    -                    root2: {id: 'root2', channel_id: 'channel2'},
    -                    comment3: {id: 'comment3', channel_id: 'channel2', root_id: 'root2'},
    -                    root3: {id: 'root3', channel_id: 'channel1'},
    -                    comment4: {id: 'comment3', channel_id: 'channel1', root_id: 'root3'},
    -                });
    +            const prevPosts = toPostsRecord({
    +                root1: {id: 'root1', channel_id: 'channel1'},
    +                comment1: {id: 'comment1', channel_id: 'channel1', root_id: 'root1'},
    +                comment2: {id: 'comment2', channel_id: 'channel1', root_id: 'root1'},
    +                root2: {id: 'root2', channel_id: 'channel2'},
    +                comment3: {id: 'comment3', channel_id: 'channel2', root_id: 'root2'},
    +                root3: {id: 'root3', channel_id: 'channel1'},
    +                comment4: {id: 'comment3', channel_id: 'channel1', root_id: 'root3'},
    +            });
     
    -                const nextState = reducers.postsInThread(state, {
    -                    type: actionType,
    -                    data: {
    -                        id: 'channel1',
    -                        viewArchivedChannels: false,
    -                    },
    -                }, prevPosts);
    +            const nextState = reducers.postsInThread(state, {
    +                type: ChannelTypes.LEAVE_CHANNEL,
    +                data: {
    +                    id: 'channel1',
    +                    viewArchivedChannels: false,
    +                },
    +            }, prevPosts);
     
    -                expect(nextState).not.toBe(state);
    -                expect(nextState.root2).toBe(state.root2);
    -                expect(nextState).toEqual({
    -                    root2: ['comment3'],
    -                });
    +            expect(nextState).not.toBe(state);
    +            expect(nextState.root2).toBe(state.root2);
    +            expect(nextState).toEqual({
    +                root2: ['comment3'],
                 });
    +        });
     
    -            it('should do nothing if no threads in that channel are loaded', () => {
    -                const state = deepFreeze({
    -                    root1: ['comment1', 'comment2'],
    -                });
    +        it('should do nothing if no threads in that channel are loaded', () => {
    +            const state = deepFreeze({
    +                root1: ['comment1', 'comment2'],
    +            });
     
    -                const prevPosts = toPostsRecord({
    -                    root1: {id: 'root1', channel_id: 'channel1'},
    -                    comment1: {id: 'comment1', channel_id: 'channel1', root_id: 'root1'},
    -                    comment2: {id: 'comment2', channel_id: 'channel1', root_id: 'root1'},
    -                });
    +            const prevPosts = toPostsRecord({
    +                root1: {id: 'root1', channel_id: 'channel1'},
    +                comment1: {id: 'comment1', channel_id: 'channel1', root_id: 'root1'},
    +                comment2: {id: 'comment2', channel_id: 'channel1', root_id: 'root1'},
    +            });
     
    -                const nextState = reducers.postsInThread(state, {
    -                    type: actionType,
    -                    data: {
    -                        id: 'channel2',
    -                        viewArchivedChannels: false,
    -                    },
    -                }, prevPosts);
    +            const nextState = reducers.postsInThread(state, {
    +                type: ChannelTypes.LEAVE_CHANNEL,
    +                data: {
    +                    id: 'channel2',
    +                    viewArchivedChannels: false,
    +                },
    +            }, prevPosts);
     
    -                expect(nextState).toBe(state);
    -                expect(nextState).toEqual({
    -                    root1: ['comment1', 'comment2'],
    -                });
    +            expect(nextState).toBe(state);
    +            expect(nextState).toEqual({
    +                root1: ['comment1', 'comment2'],
                 });
    +        });
     
    -            it('should not remove any posts with viewArchivedChannels enabled', () => {
    -                const state = deepFreeze({
    -                    root1: ['comment1', 'comment2'],
    -                    root2: ['comment3'],
    -                });
    +        it('should remove threads unconditionally (ignoring viewArchivedChannels flag)', () => {
    +            const state = deepFreeze({
    +                root1: ['comment1', 'comment2'],
    +                root2: ['comment3'],
    +            });
     
    -                const prevPosts = toPostsRecord({
    -                    root1: {id: 'root1', channel_id: 'channel1'},
    -                    comment1: {id: 'comment1', channel_id: 'channel1', root_id: 'root1'},
    -                    comment2: {id: 'comment2', channel_id: 'channel1', root_id: 'root1'},
    -                    root2: {id: 'root2', channel_id: 'channel2'},
    -                    comment3: {id: 'comment3', channel_id: 'channel2', root_id: 'root2'},
    -                });
    +            const prevPosts = toPostsRecord({
    +                root1: {id: 'root1', channel_id: 'channel1'},
    +                comment1: {id: 'comment1', channel_id: 'channel1', root_id: 'root1'},
    +                comment2: {id: 'comment2', channel_id: 'channel1', root_id: 'root1'},
    +                root2: {id: 'root2', channel_id: 'channel2'},
    +                comment3: {id: 'comment3', channel_id: 'channel2', root_id: 'root2'},
    +            });
     
    -                const nextState = reducers.postsInThread(state, {
    -                    type: actionType,
    -                    data: {
    -                        id: 'channel1',
    -                        viewArchivedChannels: true,
    -                    },
    -                }, prevPosts);
    +            const nextState = reducers.postsInThread(state, {
    +                type: ChannelTypes.LEAVE_CHANNEL,
    +                data: {
    +                    id: 'channel1',
    +                    viewArchivedChannels: true,
    +                },
    +            }, prevPosts);
     
    -                expect(nextState).toBe(state);
    -                expect(nextState).toEqual({
    -                    root1: ['comment1', 'comment2'],
    -                    root2: ['comment3'],
    -                });
    +            expect(nextState).not.toBe(state);
    +            expect(nextState.root2).toBe(state.root2);
    +            expect(nextState).toEqual({
    +                root2: ['comment3'],
                 });
    +        });
     
    -            it('should not error if a post is missing from prevPosts', () => {
    -                const state = deepFreeze({
    -                    root1: ['comment1'],
    -                });
    +        it('should not error if a post is missing from prevPosts', () => {
    +            const state = deepFreeze({
    +                root1: ['comment1'],
    +            });
     
    -                const prevPosts = toPostsRecord({
    -                    comment1: {id: 'comment1', channel_id: 'channel1', root_id: 'root1'},
    -                });
    +            const prevPosts = toPostsRecord({
    +                comment1: {id: 'comment1', channel_id: 'channel1', root_id: 'root1'},
    +            });
     
    -                const nextState = reducers.postsInThread(state, {
    -                    type: actionType,
    -                    data: {
    -                        id: 'channel1',
    -                        viewArchivedChannels: false,
    -                    },
    -                }, prevPosts);
    +            const nextState = reducers.postsInThread(state, {
    +                type: ChannelTypes.LEAVE_CHANNEL,
    +                data: {
    +                    id: 'channel1',
    +                    viewArchivedChannels: false,
    +                },
    +            }, prevPosts);
     
    -                expect(nextState).toBe(state);
    -            });
    +            expect(nextState).toBe(state);
             });
    -    }
    +    });
     });
     
     describe('removeUnneededMetadata', () => {
    @@ -4382,11 +4360,6 @@ describe('limitedViews', () => {
             PostTypes.RECEIVED_POSTS_SINCE,
             PostTypes.RECEIVED_POSTS_IN_CHANNEL,
         ];
    -    const forgetChannelActions = [
    -        ChannelTypes.RECEIVED_CHANNEL_DELETED,
    -        ChannelTypes.DELETE_CHANNEL_SUCCESS,
    -        ChannelTypes.LEAVE_CHANNEL,
    -    ];
     
         receivedPostActions.forEach((action) => {
             it(`${action} does nothing if all posts are accessible`, () => {
    @@ -4476,45 +4449,30 @@ describe('limitedViews', () => {
             expect(nextState).toEqual(initialState);
         });
     
    -    forgetChannelActions.forEach((action) => {
    -        const initialState = {...zeroState, channels: {channelId: 123}};
    -
    -        it(`${action} does nothing if archived channel is still visible`, () => {
    +    describe('LEAVE_CHANNEL in limitedViews', () => {
    +        it('should remove channel from limitedViews when leaving', () => {
    +            const initialState = {...zeroState, channels: {channelId: 123}};
                 const nextState = reducers.limitedViews(initialState, {
    -                type: action,
    +                type: ChannelTypes.LEAVE_CHANNEL,
                     data: {
    -                    viewArchivedChannels: true,
                         id: 'channelId',
                     },
                 });
     
    -            expect(nextState).toEqual(initialState);
    +            expect(nextState).toEqual(zeroState);
             });
     
    -        it(`${action} does nothing if archived channel is not limited`, () => {
    +        it('should do nothing if channel is not in limitedViews', () => {
    +            const initialState = {...zeroState, channels: {channelId: 123}};
                 const nextState = reducers.limitedViews(initialState, {
    -                type: action,
    +                type: ChannelTypes.LEAVE_CHANNEL,
                     data: {
    -                    id: 'channelId2',
    +                    id: 'nonExistentChannelId',
                     },
                 });
     
                 expect(nextState).toEqual(initialState);
    -
    -            // e.g. old state should have been returned;
    -            // reference equality should have been preserved
    -            expect(nextState).toBe(initialState);
    -        });
    -
    -        it(`${action} removes deleted channel`, () => {
    -            const nextState = reducers.limitedViews(initialState, {
    -                type: action,
    -                data: {
    -                    id: 'channelId',
    -                },
    -            });
    -
    -            expect(nextState).toEqual(zeroState);
    +            expect(nextState).toBe(initialState); // reference equality preserved
             });
         });
     });
    
  • webapp/channels/src/packages/mattermost-redux/src/reducers/entities/posts.ts+3 31 modified
    @@ -279,19 +279,12 @@ export function handlePosts(state: IDMappedObjects<Post> = {}, action: MMReduxAc
             };
         }
     
    -    case ChannelTypes.RECEIVED_CHANNEL_DELETED:
    -    case ChannelTypes.DELETE_CHANNEL_SUCCESS:
         case ChannelTypes.LEAVE_CHANNEL: {
    -        if (action.data && action.data.viewArchivedChannels) {
    -            // Nothing to do since we still want to store posts in archived channels
    -            return state;
    -        }
    -
             const channelId = action.data.id;
     
             let postDeleted = false;
     
    -        // Remove any posts in the deleted channel
    +        // Remove any posts from the channel left by the user
             const nextState = {...state};
             for (const post of Object.values(state)) {
                 if (post.channel_id === channelId) {
    @@ -604,8 +597,8 @@ export function postsInChannel(state: Record<string, PostOrderBlock[]> = {}, act
                     const recentBlock = postsForChannel[recentBlockIndex];
     
                     if (recentBlock.order.length === order.length &&
    -                    recentBlock.order[0] === order[0] &&
    -                    recentBlock.order[recentBlock.order.length - 1] === order[order.length - 1]) {
    +                        recentBlock.order[0] === order[0] &&
    +                        recentBlock.order[recentBlock.order.length - 1] === order[order.length - 1]) {
                         // The newly received posts are identical to the most recent block, so there's nothing to do
                         return state;
                     }
    @@ -839,14 +832,7 @@ export function postsInChannel(state: Record<string, PostOrderBlock[]> = {}, act
             };
         }
     
    -    case ChannelTypes.RECEIVED_CHANNEL_DELETED:
    -    case ChannelTypes.DELETE_CHANNEL_SUCCESS:
         case ChannelTypes.LEAVE_CHANNEL: {
    -        if (action.data && action.data.viewArchivedChannels) {
    -            // Nothing to do since we still want to store posts in archived channels
    -            return state;
    -        }
    -
             const channelId = action.data.id;
     
             if (!state[channelId]) {
    @@ -1116,14 +1102,7 @@ export function postsInThread(state: RelationOneToMany<Post, Post> = {}, action:
             return nextState;
         }
     
    -    case ChannelTypes.RECEIVED_CHANNEL_DELETED:
    -    case ChannelTypes.DELETE_CHANNEL_SUCCESS:
         case ChannelTypes.LEAVE_CHANNEL: {
    -        if (action.data && action.data.viewArchivedChannels) {
    -            // Nothing to do since we still want to store posts in archived channels
    -            return state;
    -        }
    -
             const channelId = action.data.id;
     
             let postDeleted = false;
    @@ -1534,14 +1513,7 @@ export function limitedViews(
             }
             return state;
         }
    -    case ChannelTypes.RECEIVED_CHANNEL_DELETED:
    -    case ChannelTypes.DELETE_CHANNEL_SUCCESS:
         case ChannelTypes.LEAVE_CHANNEL: {
    -        if (action.data && action.data.viewArchivedChannels) {
    -            // Nothing to do since we still want to store posts in archived channels
    -            return state;
    -        }
    -
             const channelId = action.data.id;
             if (!state.channels[channelId]) {
                 return state;
    
  • webapp/channels/src/packages/mattermost-redux/src/reducers/entities/threads/index.ts+2 7 modified
    @@ -171,13 +171,8 @@ function reducer(state: ThreadsState = initialState, action: MMReduxAction): Thr
         };
     
         // acting as a 'middleware'
    -    if (
    -        action.type === ChannelTypes.LEAVE_CHANNEL ||
    -        action.type === ChannelTypes.RECEIVED_CHANNEL_DELETED
    -    ) {
    -        if (!action.data.viewArchivedChannels) {
    -            extra.threadsToDelete = getThreadsOfChannel(state.threads, action.data.id);
    -        }
    +    if (action.type === ChannelTypes.LEAVE_CHANNEL) {
    +        extra.threadsToDelete = getThreadsOfChannel(state.threads, action.data.id);
         }
     
         const nextState = {
    
  • webapp/platform/types/src/config.ts+0 2 modified
    @@ -123,7 +123,6 @@ export type ClientConfig = {
         ExperimentalEnablePostMetadata: string;
         ExperimentalGroupUnreadChannels: string;
         ExperimentalPrimaryTeam: string;
    -    ExperimentalViewArchivedChannels: string;
         FileLevel: string;
         FeatureFlagAppsEnabled: string;
         FeatureFlagCallsEnabled: string;
    @@ -436,7 +435,6 @@ export type TeamSettings = {
         MaxNotificationsPerChannel: number;
         EnableConfirmNotificationsToChannel: boolean;
         TeammateNameDisplay: string;
    -    ExperimentalViewArchivedChannels: boolean;
         ExperimentalEnableAutomaticReplies: boolean;
         LockTeammateNameDisplay: boolean;
         ExperimentalPrimaryTeam: string;
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.