Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
ea514b7
Search member in room using API
Rohit3523 Jan 21, 2026
fd7e0fe
Guard against out‑of‑order search responses
Rohit3523 Jan 21, 2026
ad736f7
Lint fix
Rohit3523 Jan 21, 2026
a3aeda5
chore: format code and fix lint issues [skip ci]
Rohit3523 Jan 21, 2026
3820bd7
merge search result
Rohit3523 Jan 22, 2026
f2edc75
use debounce hook
Rohit3523 Jan 23, 2026
ae0fbbc
Merge branch 'develop' into search-filter-fix
Rohit3523 Jan 23, 2026
d4839a8
Prevent stale member updates when the filter changes or clears
Rohit3523 Jan 23, 2026
cca7ef9
Merge branch 'search-filter-fix' of https://github.com/RocketChat/Roc…
Rohit3523 Jan 23, 2026
1492bec
Use members from state
Rohit3523 Jan 23, 2026
55cfd8b
Added E2E test for search member
Rohit3523 Jan 23, 2026
4f36a02
combine filter text with status filter
Rohit3523 Jan 23, 2026
cc8f8b4
test update
Rohit3523 Jan 23, 2026
cd9210a
Test update
Rohit3523 Jan 24, 2026
831d6d0
status filter is not using search text
Rohit3523 Jan 24, 2026
a0a2ec8
chore: format code and fix lint issues [skip ci]
Rohit3523 Jan 24, 2026
8bcb82c
Added state room in effect
Rohit3523 Jan 26, 2026
47da4ec
chore: code improvements
OtavioStasiak Jan 26, 2026
d852979
chore: format code and fix lint issues [skip ci]
OtavioStasiak Jan 26, 2026
8a0316b
chore: code improvements
OtavioStasiak Jan 26, 2026
98f0fad
Merge branch 'develop' into search-filter-fix
OtavioStasiak Jan 26, 2026
2cf2053
chore: format code and fix lint issues [skip ci]
OtavioStasiak Jan 26, 2026
806e79e
fix: remove duplicated fetch roles and remove members from state of u…
OtavioStasiak Jan 27, 2026
f207cb7
fix: remove duplicated useEffect and use primitive values in fetchRol…
OtavioStasiak Jan 27, 2026
33265f0
chore: format code and fix lint issues [skip ci]
OtavioStasiak Jan 27, 2026
503bc9f
fix: remove array from useEffect dependencies
OtavioStasiak Jan 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions .maestro/tests/room/search-member.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
appId: chat.rocket.reactnative
name: Search Member
onFlowStart:
- runFlow: '../../helpers/setup.yaml'
tags:
- test-13

---
- evalScript: ${output.user = output.utils.createUser()}

- runFlow:
file: '../../helpers/login-with-deeplink.yaml'
env:
USERNAME: ${output.user.username}
PASSWORD: ${output.user.password}

- runFlow:
file: '../../helpers/navigate-to-room.yaml'
env:
ROOM: 'general'
- tapOn:
id: room-header
- extendedWaitUntil:
visible:
id: 'room-actions-view'
timeout: 60000
- tapOn:
id: 'room-actions-members'
- extendedWaitUntil:
visible:
id: 'room-members-view-search'
timeout: 60000

# should search in all users
- tapOn:
id: room-members-view-search
- inputText: rohit.bansal
- extendedWaitUntil:
visible:
id: 'room-members-view-item-rohit.bansal'
timeout: 60000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing cases like search and filter.


# use online status and it should use the text filter
- tapOn:
id: room-members-view-filter
- extendedWaitUntil:
visible:
id: 'room-members-view-toggle-status-online'
timeout: 60000
- tapOn:
id: room-members-view-toggle-status-online
- extendedWaitUntil:
visible:
text: 'No members found'
timeout: 60000

# use all status again and it should use text filter
- tapOn:
id: room-members-view-filter
- extendedWaitUntil:
visible:
id: 'room-members-view-toggle-status-all'
timeout: 60000
- tapOn:
id: room-members-view-toggle-status-all
- extendedWaitUntil:
visible:
id: 'room-members-view-item-rohit.bansal'
timeout: 60000
- tapOn:
id: clear-text-input

- evalScript: ${output.secondUser = output.utils.createUser()}

# should search for new user in all list
- tapOn:
id: room-members-view-search
- inputText: ${output.secondUser.username}
- extendedWaitUntil:
visible:
id: 'room-members-view-item-${output.secondUser.username}'
timeout: 60000

# Verify "No members found" message appears correctly when search returns no results
- tapOn:
id: room-members-view-search
- inputText: nonexistentuser12345
- extendedWaitUntil:
visible:
text: 'No members found'
timeout: 60000
178 changes: 104 additions & 74 deletions app/views/RoomMembersView/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type NavigationProp, type RouteProp, useNavigation, useRoute } from '@react-navigation/native';
import React, { useEffect, useReducer } from 'react';
import React, { useCallback, useEffect, useReducer, useRef } from 'react';
import { FlatList, Text, View } from 'react-native';
import { shallowEqual } from 'react-redux';

Expand All @@ -17,7 +17,7 @@ import { type IGetRoomRoles, type TSubscriptionModel, type TUserModel } from '..
import I18n from '../../i18n';
import { useAppSelector } from '../../lib/hooks/useAppSelector';
import { usePermissions } from '../../lib/hooks/usePermissions';
import { compareServerVersion, getRoomTitle, isGroupChat } from '../../lib/methods/helpers';
import { compareServerVersion, getRoomTitle, isGroupChat, useDebounce } from '../../lib/methods/helpers';
import { handleIgnore } from '../../lib/methods/helpers/handleIgnore';
import { showConfirmationAlert } from '../../lib/methods/helpers/info';
import log from '../../lib/methods/helpers/log';
Expand All @@ -41,7 +41,6 @@ import {
type TRoomType
} from './helpers';
import styles from './styles';
import { sanitizeLikeString } from '../../lib/database/utils';

const PAGE_SIZE = 25;

Expand Down Expand Up @@ -76,6 +75,8 @@ const RoomMembersView = (): React.ReactElement => {
const { params } = useRoute<RouteProp<ModalStackParamList, 'RoomMembersView'>>();
const navigation = useNavigation<NavigationProp<ModalStackParamList, 'RoomMembersView'>>();

const latestSearchRequest = useRef(0);

const { isMasterDetail, serverVersion, useRealName, user, loading } = useAppSelector(
state => ({
isMasterDetail: state.app.isMasterDetail,
Expand All @@ -95,7 +96,7 @@ const RoomMembersView = (): React.ReactElement => {
(state: IRoomMembersViewState, newState: Partial<IRoomMembersViewState>) => ({ ...state, ...newState }),
{
isLoading: false,
allUsers: false,
allUsers: true,
filtering: '',
members: [],
room: params.room || ({} as TSubscriptionModel),
Expand Down Expand Up @@ -123,38 +124,84 @@ const RoomMembersView = (): React.ReactElement => {

useEffect(() => {
const subscription = params?.room?.observe && params.room.observe().subscribe(changes => updateState({ room: changes }));
setHeader(false);
fetchMembers(false);
setHeader(true);
return () => subscription?.unsubscribe();
}, []);

const fetchRoles = () => {
if (isGroupChat(state.room)) {
return;
}
if (
muteUserPermission ||
setLeaderPermission ||
setOwnerPermission ||
setModeratorPermission ||
removeUserPermission ||
editTeamMemberPermission ||
viewAllTeamChannelsPermission ||
viewAllTeamsPermission
) {
fetchRoomMembersRoles(state.room.t as any, state.room.rid, updateState);
}
};

const fetchMembers = useCallback(async () => {
const { members, isLoading, end, room, filter, page, allUsers } = state;
const { t } = room;

if (isLoading || end) {
return;
}

const requestId = ++latestSearchRequest.current;
updateState({ isLoading: true });

try {
const membersResult = await getRoomMembers({
rid: room.rid,
roomType: t,
type: allUsers ? 'all' : 'online',
filter,
skip: PAGE_SIZE * page,
limit: PAGE_SIZE,
allUsers
});

if (requestId !== latestSearchRequest.current) {
return;
}

const existingIds = new Set(members.map(m => m._id));
const membersResultFiltered = membersResult?.filter((member: TUserModel) => !existingIds.has(member._id));

// Safety check: if page is 0, we replace the list entirely
const newMembers = page === 0 ? membersResultFiltered : [...members, ...(membersResultFiltered || [])];
const isEnd = membersResult?.length < PAGE_SIZE;

updateState({
members: newMembers,
isLoading: false,
end: isEnd,
page: page + 1
});
} catch (e) {
log(e);
if (requestId === latestSearchRequest.current) {
updateState({ isLoading: false });
}
}
}, [state.isLoading, state.end, state.room.t, state.filter, state.page, state.allUsers]);

useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
const { allUsers } = state;
fetchMembers(allUsers);
fetchMembers();
});

return unsubscribe;
}, [navigation]);

useEffect(() => {
const fetchRoles = () => {
if (isGroupChat(state.room)) {
return;
}
if (
muteUserPermission ||
setLeaderPermission ||
setOwnerPermission ||
setModeratorPermission ||
removeUserPermission ||
editTeamMemberPermission ||
viewAllTeamChannelsPermission ||
viewAllTeamsPermission
) {
fetchRoomMembersRoles(state.room.t as any, state.room.rid, updateState);
}
};
fetchRoles();
}, [
muteUserPermission,
Expand All @@ -164,13 +211,35 @@ const RoomMembersView = (): React.ReactElement => {
removeUserPermission,
editTeamMemberPermission,
viewAllTeamChannelsPermission,
viewAllTeamsPermission
viewAllTeamsPermission,
state.room?.rid,
state.room?.t
]);

useEffect(() => {
fetchMembers();
}, [state.filter, state.allUsers]);

const debounceFilterChange = useDebounce((text: string) => {
const trimmedFilter = text.trim();

if (!trimmedFilter) {
latestSearchRequest.current += 1;
}

updateState({
filter: trimmedFilter,
page: 0,
members: [],
end: false,
isLoading: false
});
}, 500);

const toggleStatus = (status: boolean) => {
try {
updateState({ members: [], allUsers: status, end: false });
fetchMembers(status);
// We only update 'allUsers'. 'filter' remains in state, so the next fetch uses both.
updateState({ members: [], allUsers: status, end: false, page: 0 });
setHeader(status);
} catch (e) {
log(e);
Expand All @@ -189,14 +258,14 @@ const RoomMembersView = (): React.ReactElement => {
options: [
{
title: I18n.t('Online'),
onPress: () => toggleStatus(true),
right: () => <Radio check={allUsers} />,
onPress: () => toggleStatus(false),
right: () => <Radio check={!allUsers} />,
testID: 'room-members-view-toggle-status-online'
},
{
title: I18n.t('All'),
onPress: () => toggleStatus(false),
right: () => <Radio check={!allUsers} />,
onPress: () => toggleStatus(true),
right: () => <Radio check={allUsers} />,
testID: 'room-members-view-toggle-status-all'
}
]
Expand Down Expand Up @@ -348,49 +417,10 @@ const RoomMembersView = (): React.ReactElement => {
});
};

const fetchMembers = async (status: boolean) => {
const { members, isLoading, end, room, filter, page } = state;
const { t } = room;

if (isLoading || end) {
return;
}

updateState({ isLoading: true });
try {
const membersResult = await getRoomMembers({
rid: room.rid,
roomType: t,
type: !status ? 'all' : 'online',
filter,
skip: PAGE_SIZE * page,
limit: PAGE_SIZE,
allUsers: !status
});
const end = membersResult?.length < PAGE_SIZE;
const membersResultFiltered = membersResult?.filter((member: TUserModel) => !members.some(m => m._id === member._id));
updateState({
members: [...members, ...membersResultFiltered],
isLoading: false,
end,
page: page + 1
});
} catch (e) {
log(e);
updateState({ isLoading: false });
}
};

const filter = sanitizeLikeString(state.filter.toLowerCase()) || '';
const filteredMembers =
state.members && state.members.length > 0 && state.filter
? state.members.filter(m => m.username.toLowerCase().match(filter) || m.name?.toLowerCase().match(filter))
: null;

return (
<SafeAreaView testID='room-members-view'>
<FlatList
data={filteredMembers || state.members}
data={state.members}
renderItem={({ item }) => (
<View style={{ backgroundColor: colors.surfaceRoom }}>
<UserItem
Expand All @@ -412,12 +442,12 @@ const RoomMembersView = (): React.ReactElement => {
t={state.room.t}
abacAttributes={state.room.abacAttributes}
/>
<SearchBox onChangeText={text => updateState({ filter: text.trim() })} testID='room-members-view-search' />
<SearchBox onChangeText={text => debounceFilterChange(text)} testID='room-members-view-search' />
</>
}
ListFooterComponent={() => (state.isLoading ? <ActivityIndicator /> : null)}
onEndReachedThreshold={0.1}
onEndReached={() => fetchMembers(state.allUsers)}
onEndReached={() => fetchMembers()}
ListEmptyComponent={() =>
state.end ? (
<Text style={[styles.noResult, { color: colors.fontTitlesLabels }]}>{I18n.t('No_members_found')}</Text>
Expand Down
Loading