diff --git a/app/components/server_user_list/index.tsx b/app/components/server_user_list/index.tsx index 70e5d64a4..079c82931 100644 --- a/app/components/server_user_list/index.tsx +++ b/app/components/server_user_list/index.tsx @@ -12,7 +12,6 @@ import type {AvailableScreens} from '@typings/screens/navigation'; import type {SectionListData} from 'react-native'; type Props = { - currentUserId: string; tutorialWatched: boolean; handleSelectProfile: (user: UserProfile) => void; term: string; @@ -26,7 +25,6 @@ type Props = { } export default function ServerUserList({ - currentUserId, tutorialWatched, handleSelectProfile, term, @@ -123,7 +121,6 @@ export default function ServerUserList({ return ( { return { profiles: [], testID: 'UserListRow', - currentUserId: '1', handleSelectProfile: jest.fn(), fetchMore: jest.fn(), loading: true, diff --git a/app/components/user_list/index.tsx b/app/components/user_list/index.tsx index 1007ac213..e5a78bea1 100644 --- a/app/components/user_list/index.tsx +++ b/app/components/user_list/index.tsx @@ -172,7 +172,6 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { type Props = { profiles: UserProfile[]; channelMembers?: ChannelMembership[]; - currentUserId: string; handleSelectProfile: (user: UserProfile | UserModel) => void; fetchMore?: () => void; loading: boolean; @@ -192,7 +191,6 @@ export default function UserList({ profiles, channelMembers, selectedIds, - currentUserId, handleSelectProfile, fetchMore, loading, @@ -262,7 +260,6 @@ export default function UserList({ highlight={section?.first && index === 0} id={item.id} isChannelAdmin={isChAdmin} - isMyUser={currentUserId === item.id} manageMode={manageMode} onPress={handleSelectProfile} onLongPress={openUserProfile} @@ -276,7 +273,7 @@ export default function UserList({ includeMargin={includeUserMargin} /> ); - }, [selectedIds, currentUserId, manageMode, handleSelectProfile, openUserProfile, showManageMode, tutorialWatched, includeUserMargin]); + }, [selectedIds, manageMode, handleSelectProfile, openUserProfile, showManageMode, tutorialWatched, includeUserMargin]); const renderLoading = useCallback(() => { if (!loading) { @@ -290,7 +287,7 @@ export default function UserList({ size='large' /> ); - }, [loading, theme]); + }, [loading, style.loadingContainer, theme.buttonBg]); const renderNoResults = useCallback(() => { if (!showNoResults || !term) { @@ -302,7 +299,7 @@ export default function UserList({ ); - }, [showNoResults && style, term, noResutsStyle]); + }, [showNoResults, term, noResutsStyle]); const renderSectionHeader = useCallback(({section}: {section: SectionListData}) => { return ( diff --git a/app/components/user_list_row/index.tsx b/app/components/user_list_row/index.tsx index e180d710d..d40eb004c 100644 --- a/app/components/user_list_row/index.tsx +++ b/app/components/user_list_row/index.tsx @@ -26,7 +26,6 @@ type Props = { highlight?: boolean; id: string; includeMargin?: boolean; - isMyUser: boolean; isChannelAdmin: boolean; manageMode: boolean; onLongPress: (user: UserProfile | UserModel) => void; @@ -82,7 +81,6 @@ const messages = defineMessages({ function UserListRow({ id, includeMargin, - isMyUser, highlight, isChannelAdmin, onPress, @@ -129,7 +127,7 @@ function UserListRow({ }, [onPress]); const manageModeIcon = useMemo(() => { - if (!showManageMode || isMyUser) { + if (!showManageMode) { return null; } @@ -149,7 +147,7 @@ function UserListRow({ /> ); - }, [isChannelAdmin, isMyUser, showManageMode, style.manageText, style.selectorManage, theme.centerChannelColor]); + }, [isChannelAdmin, showManageMode, style.manageText, style.selectorManage, theme.centerChannelColor]); const onLayout = useCallback(() => { if (highlight && !tutorialWatched) { diff --git a/app/products/playbooks/screens/select_user/index.test.tsx b/app/products/playbooks/screens/select_user/index.test.tsx index f87c1bbcd..38002ee14 100644 --- a/app/products/playbooks/screens/select_user/index.test.tsx +++ b/app/products/playbooks/screens/select_user/index.test.tsx @@ -54,24 +54,6 @@ describe('SelectUser', () => { const selectUser = getByTestId('select-user'); // Default values from observables when no data exists - expect(selectUser.props.currentUserId).toBe(''); - expect(selectUser.props.currentTeamId).toBe(''); - }); - - it('should render correctly with current user data', async () => { - await operator.handleSystem({ - systems: [{ - id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, - value: 'current-user-id', - }], - prepareRecordsOnly: false, - }); - - const props = getBaseProps(); - const {getByTestId} = renderWithEverything(, {database}); - - const selectUser = getByTestId('select-user'); - expect(selectUser.props.currentUserId).toBe('current-user-id'); expect(selectUser.props.currentTeamId).toBe(''); }); @@ -89,39 +71,11 @@ describe('SelectUser', () => { const selectUser = getByTestId('select-user'); expect(selectUser.props.currentTeamId).toBe('current-team-id'); - expect(selectUser.props.currentUserId).toBe(''); - }); - - it('should render correctly with both current user and team data', async () => { - await operator.handleSystem({ - systems: [ - { - id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, - value: 'current-user-id', - }, - { - id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, - value: 'current-team-id', - }, - ], - prepareRecordsOnly: false, - }); - - const props = getBaseProps(); - const {getByTestId} = renderWithEverything(, {database}); - - const selectUser = getByTestId('select-user'); - expect(selectUser.props.currentUserId).toBe('current-user-id'); - expect(selectUser.props.currentTeamId).toBe('current-team-id'); }); it('should update observables when data changes', async () => { await operator.handleSystem({ systems: [ - { - id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, - value: 'current-user-id', - }, { id: SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID, value: 'current-team-id', @@ -134,25 +88,8 @@ describe('SelectUser', () => { const {getByTestId} = renderWithEverything(, {database}); const selectUser = getByTestId('select-user'); - expect(selectUser.props.currentUserId).toBe('current-user-id'); expect(selectUser.props.currentTeamId).toBe('current-team-id'); - await act(async () => { - // Update current user ID - await operator.handleSystem({ - systems: [{ - id: SYSTEM_IDENTIFIERS.CURRENT_USER_ID, - value: 'new-user-id', - }], - prepareRecordsOnly: false, - }); - }); - - await waitFor(() => { - expect(selectUser.props.currentUserId).toBe('new-user-id'); - expect(selectUser.props.currentTeamId).toBe('current-team-id'); - }); - await act(async () => { // Update current team ID await operator.handleSystem({ @@ -165,7 +102,6 @@ describe('SelectUser', () => { }); await waitFor(() => { - expect(selectUser.props.currentUserId).toBe('new-user-id'); expect(selectUser.props.currentTeamId).toBe('new-team-id'); }); }); diff --git a/app/products/playbooks/screens/select_user/index.ts b/app/products/playbooks/screens/select_user/index.ts index fad44e936..aadf19fa0 100644 --- a/app/products/playbooks/screens/select_user/index.ts +++ b/app/products/playbooks/screens/select_user/index.ts @@ -2,14 +2,13 @@ // See LICENSE.txt for license information. import {withDatabase, withObservables} from '@nozbe/watermelondb/react'; -import {observeCurrentTeamId, observeCurrentUserId} from '@queries/servers/system'; +import {observeCurrentTeamId} from '@queries/servers/system'; import SelectUser from './select_user'; import type {WithDatabaseArgs} from '@typings/database/database'; const withTeamId = withObservables([], ({database}: WithDatabaseArgs) => ({ - currentUserId: observeCurrentUserId(database), currentTeamId: observeCurrentTeamId(database), })); diff --git a/app/products/playbooks/screens/select_user/select_user.test.tsx b/app/products/playbooks/screens/select_user/select_user.test.tsx index 13e41806f..b506075ce 100644 --- a/app/products/playbooks/screens/select_user/select_user.test.tsx +++ b/app/products/playbooks/screens/select_user/select_user.test.tsx @@ -63,7 +63,6 @@ describe('SelectUser', () => { function getBaseProps(): ComponentProps { return { currentTeamId: 'team-1', - currentUserId: 'current-user', handleSelect: jest.fn(), componentId: 'PlaybookSelectUser', participantIds: ['participant-1', 'participant-2'], @@ -83,7 +82,6 @@ describe('SelectUser', () => { expect(queryByTestId('button')).toBeNull(); const userList = getByTestId('integration_selector.user_list'); - expect(userList).toHaveProp('currentUserId', props.currentUserId); expect(userList).toHaveProp('term', ''); expect(userList).toHaveProp('tutorialWatched', true); expect(userList).toHaveProp('handleSelectProfile', expect.any(Function)); diff --git a/app/products/playbooks/screens/select_user/select_user.tsx b/app/products/playbooks/screens/select_user/select_user.tsx index a7cc92767..f99548491 100644 --- a/app/products/playbooks/screens/select_user/select_user.tsx +++ b/app/products/playbooks/screens/select_user/select_user.tsx @@ -29,7 +29,6 @@ const close = () => { export type Props = { currentTeamId: string; - currentUserId: string; handleSelect: (opt: UserProfile) => void; handleRemove?: () => void; selected?: string; @@ -93,7 +92,6 @@ function SelectUser({ handleSelect, handleRemove, currentTeamId, - currentUserId, componentId, participantIds, }: Props) { @@ -251,7 +249,6 @@ function SelectUser({ )} , id: string) { export default function ChannelAddMembers({ componentId, channel, - currentUserId, teammateNameDisplay, tutorialWatched, inModal, @@ -303,7 +301,6 @@ export default function ChannelAddMembers({ /> ({ - currentUserId: observeCurrentUserId(database), currentTeamId: observeCurrentTeamId(database), })); diff --git a/app/screens/integration_selector/integration_selector.tsx b/app/screens/integration_selector/integration_selector.tsx index 7f3657257..caffc60e9 100644 --- a/app/screens/integration_selector/integration_selector.tsx +++ b/app/screens/integration_selector/integration_selector.tsx @@ -114,7 +114,6 @@ export type Props = { getDynamicOptions?: (userInput?: string) => Promise; options?: PostActionOption[]; currentTeamId: string; - currentUserId: string; data?: DataTypeList; dataSource: string; handleSelect: (opt: Selection) => void; @@ -177,7 +176,7 @@ const messages = defineMessages({ function IntegrationSelector( {dataSource, data, isMultiselect = false, selected, handleSelect, - currentTeamId, currentUserId, componentId, getDynamicOptions, options}: Props) { + currentTeamId, componentId, getDynamicOptions, options}: Props) { const serverUrl = useServerUrl(); const theme = useTheme(); const searchTimeoutId = useRef(null); @@ -576,7 +575,6 @@ function IntegrationSelector( case ViewConstants.DATA_SOURCE_USERS: return ( { setTerm(''); setSearchResults(EMPTY); @@ -123,10 +125,6 @@ export default function ManageChannelMembers({ useAndroidHardwareBackHandler(componentId, close); const handleSelectProfile = useCallback(async (profile: UserProfile) => { - if (profile.id === currentUserId && isManageMode) { - return; - } - if (profile.id !== currentUserId) { await fetchUsersByIds(serverUrl, [profile.id]); } @@ -191,7 +189,7 @@ export default function ManageChannelMembers({ text: formatMessage(manage ? messages.button_done : messages.button_manage), }], }); - }, [theme.sidebarHeaderTextColor]); + }, [componentId, formatMessage, theme.sidebarHeaderTextColor]); const toggleManageEnabled = useCallback(() => { updateNavigationButtons(!isManageMode); @@ -240,11 +238,11 @@ export default function ManageChannelMembers({ }, [searchResults, profiles, searchedTerm, sortedProfiles]); useEffect(() => { - if (!term) { + if (!hasTerm) { setSearchResults(EMPTY); setSearchedTerm(''); } - }, [Boolean(term)]); + }, [hasTerm]); useNavButtonPressed(MANAGE_BUTTON, componentId, toggleManageEnabled, [toggleManageEnabled]); @@ -283,12 +281,19 @@ export default function ManageChannelMembers({ return () => { mounted.current = false; }; + + // This effect is used only to track the mounted state and the initial fetch + // so it should only run once + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { if (canManageAndRemoveMembers) { updateNavigationButtons(false); } + + // We only want to update the navigation buttons when the permission changes + // eslint-disable-next-line react-hooks/exhaustive-deps }, [canManageAndRemoveMembers]); useEffect(() => { @@ -334,7 +339,6 @@ export default function ManageChannelMembers({ /> { + return element(by.id(`${this.testID.userItemPrefix}${userId}.${userId}`)); + }; + + getUserItemProfilePicture = (userId: string) => { + return element(ProfilePicture.getProfilePictureItemMatcher(this.testID.userItemPrefix, userId)); + }; + + getUserItemDisplayName = (userId: string) => { + return element(by.id(`${this.testID.userItemPrefix}${userId}.${userId}.display_name`)); + }; + + toBeVisible = async () => { + if (isIos()) { + await waitFor(this.manageMembersScreen).toExist().withTimeout(timeouts.TEN_SEC); + } + + return this.manageMembersScreen; + }; + + open = async () => { + // # Open channel info screen and tap on members option + await ChannelInfoScreen.membersOption.tap(); + await wait(timeouts.ONE_SEC); + return this.toBeVisible(); + }; + + close = async () => { + await this.backButton.tap(); + await expect(this.manageMembersScreen).not.toBeVisible(); + }; + + toggleManageMode = async () => { + // # Tap on manage/done button to toggle manage mode + // The button testID is the same for both states, so we use manageButton + await this.manageButton.tap(); + await wait(timeouts.ONE_SEC); + }; + + exitManageMode = async () => { + // # Tap on done button to exit manage mode + // The button testID is the same for both states, so we use doneButton (which is the same element) + await this.doneButton.tap(); + await wait(timeouts.ONE_SEC); + }; + + closeTutorial = async () => { + try { + if (isIos()) { + await waitFor(this.tutorialHighlight).toExist().withTimeout(timeouts.HALF_MIN); + await this.tutorialSwipeLeft.tap(); + await expect(this.tutorialHighlight).not.toExist(); + } else { + await wait(timeouts.ONE_SEC); + await device.pressBack(); + await wait(timeouts.ONE_SEC); + } + } catch { + // eslint-disable-next-line no-console + console.log('Tutorial element not visible, skipping action:'); + } + }; +} + +const manageChannelMembersScreen = new ManageChannelMembersScreen(); +export default manageChannelMembersScreen; + diff --git a/detox/e2e/test/products/channels/channels/manage_own_channel_membership.e2e.ts b/detox/e2e/test/products/channels/channels/manage_own_channel_membership.e2e.ts new file mode 100644 index 000000000..3c129f27d --- /dev/null +++ b/detox/e2e/test/products/channels/channels/manage_own_channel_membership.e2e.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +// ******************************************************************* +// - [#] indicates a test step (e.g. # Go to a screen) +// - [*] indicates an assertion (e.g. * Check the title) +// - Use element testID when selecting an element. Create one if none. +// ******************************************************************* + +import { + Channel, + Setup, +} from '@support/server_api'; +import { + serverOneUrl, + siteOneUrl, +} from '@support/test_config'; +import { + ChannelScreen, + ChannelListScreen, + ChannelInfoScreen, + HomeScreen, + LoginScreen, + ManageChannelMembersScreen, + ServerScreen, + UserProfileScreen, +} from '@support/ui/screen'; +import {timeouts, wait} from '@support/utils'; +import {expect} from 'detox'; + +describe('Channels - Manage Own Channel Membership', () => { + const serverOneDisplayName = 'Server 1'; + const channelsCategory = 'channels'; + let testTeam: any; + let testUser: any; + + beforeAll(async () => { + const {team, user} = await Setup.apiInit(siteOneUrl); + testTeam = team; + testUser = user; + + // # Log in to server + await ServerScreen.connectToServer(serverOneUrl, serverOneDisplayName); + await LoginScreen.login(testUser); + }); + + beforeEach(async () => { + // * Verify on channel list screen + await ChannelListScreen.toBeVisible(); + }); + + afterAll(async () => { + // # Log out + await HomeScreen.logout(); + }); + + it('MM-66375 - should be able to see and manage own membership in channel members list', async () => { + // # Create a channel and add the test user to it + const {channel} = await Channel.apiCreateChannel(siteOneUrl, {teamId: testTeam.id}); + await Channel.apiAddUserToChannel(siteOneUrl, testUser.id, channel.id); + await device.reloadReactNative(); + + // # Open the channel screen + await ChannelScreen.open(channelsCategory, channel.name); + + // # Open channel info screen + await ChannelInfoScreen.open(); + + // # Open manage channel members screen + await ChannelInfoScreen.scrollView.scrollTo('bottom'); + await wait(timeouts.ONE_SEC); + await ManageChannelMembersScreen.open(); + + // # Close tutorial + await ManageChannelMembersScreen.closeTutorial(); + + // * Verify manage channel members screen is visible + await ManageChannelMembersScreen.toBeVisible(); + + // * Verify the current user appears in the members list + await expect(ManageChannelMembersScreen.getUserItemDisplayName(testUser.id)).toBeVisible(); + + // # Enable manage mode + await ManageChannelMembersScreen.toggleManageMode(); + + // * Verify manage mode is enabled (done button should be visible) + await expect(ManageChannelMembersScreen.doneButton).toBeVisible(); + + // * Verify the current user can be selected in manage mode (they should have the manage mode icon visible) + // The manage mode icon (chevron-down) should be visible for the current user + await expect(ManageChannelMembersScreen.getUserItem(testUser.id)).toBeVisible(); + + // # Tap on the current user in manage mode + await ManageChannelMembersScreen.getUserItem(testUser.id).tap(); + + // * Verify that tapping on own user in manage mode opens the user profile or shows manage options + // This verifies that the restriction preventing users from managing their own membership has been removed + await UserProfileScreen.toBeVisible(); + + // # Close user profile screen + await UserProfileScreen.close(); + + // # Exit manage mode + await ManageChannelMembersScreen.exitManageMode(); + + // * Verify manage mode is disabled (manage button should be visible) + await expect(ManageChannelMembersScreen.manageButton).toBeVisible(); + + // # Go back to channel list screen + await ManageChannelMembersScreen.close(); + await ChannelInfoScreen.close(); + await ChannelScreen.back(); + }); +}); +