Skip to content

Commit 97008fe

Browse files
authored
fix: room member search to use server data instead of local data (#6938)
1 parent 3f4da80 commit 97008fe

2 files changed

Lines changed: 195 additions & 74 deletions

File tree

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
appId: chat.rocket.reactnative
2+
name: Search Member
3+
onFlowStart:
4+
- runFlow: '../../helpers/setup.yaml'
5+
tags:
6+
- test-13
7+
8+
---
9+
- evalScript: ${output.user = output.utils.createUser()}
10+
11+
- runFlow:
12+
file: '../../helpers/login-with-deeplink.yaml'
13+
env:
14+
USERNAME: ${output.user.username}
15+
PASSWORD: ${output.user.password}
16+
17+
- runFlow:
18+
file: '../../helpers/navigate-to-room.yaml'
19+
env:
20+
ROOM: 'general'
21+
- tapOn:
22+
id: room-header
23+
- extendedWaitUntil:
24+
visible:
25+
id: 'room-actions-view'
26+
timeout: 60000
27+
- tapOn:
28+
id: 'room-actions-members'
29+
- extendedWaitUntil:
30+
visible:
31+
id: 'room-members-view-search'
32+
timeout: 60000
33+
34+
# should search in all users
35+
- tapOn:
36+
id: room-members-view-search
37+
- inputText: rohit.bansal
38+
- extendedWaitUntil:
39+
visible:
40+
id: 'room-members-view-item-rohit.bansal'
41+
timeout: 60000
42+
43+
# use online status and it should use the text filter
44+
- tapOn:
45+
id: room-members-view-filter
46+
- extendedWaitUntil:
47+
visible:
48+
id: 'room-members-view-toggle-status-online'
49+
timeout: 60000
50+
- tapOn:
51+
id: room-members-view-toggle-status-online
52+
- extendedWaitUntil:
53+
visible:
54+
text: 'No members found'
55+
timeout: 60000
56+
57+
# use all status again and it should use text filter
58+
- tapOn:
59+
id: room-members-view-filter
60+
- extendedWaitUntil:
61+
visible:
62+
id: 'room-members-view-toggle-status-all'
63+
timeout: 60000
64+
- tapOn:
65+
id: room-members-view-toggle-status-all
66+
- extendedWaitUntil:
67+
visible:
68+
id: 'room-members-view-item-rohit.bansal'
69+
timeout: 60000
70+
- tapOn:
71+
id: clear-text-input
72+
73+
- evalScript: ${output.secondUser = output.utils.createUser()}
74+
75+
# should search for new user in all list
76+
- tapOn:
77+
id: room-members-view-search
78+
- inputText: ${output.secondUser.username}
79+
- extendedWaitUntil:
80+
visible:
81+
id: 'room-members-view-item-${output.secondUser.username}'
82+
timeout: 60000
83+
84+
# Verify "No members found" message appears correctly when search returns no results
85+
- tapOn:
86+
id: room-members-view-search
87+
- inputText: nonexistentuser12345
88+
- extendedWaitUntil:
89+
visible:
90+
text: 'No members found'
91+
timeout: 60000

app/views/RoomMembersView/index.tsx

Lines changed: 104 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { type NavigationProp, type RouteProp, useNavigation, useRoute } from '@react-navigation/native';
2-
import React, { useEffect, useReducer } from 'react';
2+
import React, { useCallback, useEffect, useReducer, useRef } from 'react';
33
import { FlatList, Text, View } from 'react-native';
44
import { shallowEqual } from 'react-redux';
55

@@ -17,7 +17,7 @@ import { type IGetRoomRoles, type TSubscriptionModel, type TUserModel } from '..
1717
import I18n from '../../i18n';
1818
import { useAppSelector } from '../../lib/hooks/useAppSelector';
1919
import { usePermissions } from '../../lib/hooks/usePermissions';
20-
import { compareServerVersion, getRoomTitle, isGroupChat } from '../../lib/methods/helpers';
20+
import { compareServerVersion, getRoomTitle, isGroupChat, useDebounce } from '../../lib/methods/helpers';
2121
import { handleIgnore } from '../../lib/methods/helpers/handleIgnore';
2222
import { showConfirmationAlert } from '../../lib/methods/helpers/info';
2323
import log from '../../lib/methods/helpers/log';
@@ -41,7 +41,6 @@ import {
4141
type TRoomType
4242
} from './helpers';
4343
import styles from './styles';
44-
import { sanitizeLikeString } from '../../lib/database/utils';
4544

4645
const PAGE_SIZE = 25;
4746

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

78+
const latestSearchRequest = useRef(0);
79+
7980
const { isMasterDetail, serverVersion, useRealName, user, loading } = useAppSelector(
8081
state => ({
8182
isMasterDetail: state.app.isMasterDetail,
@@ -95,7 +96,7 @@ const RoomMembersView = (): React.ReactElement => {
9596
(state: IRoomMembersViewState, newState: Partial<IRoomMembersViewState>) => ({ ...state, ...newState }),
9697
{
9798
isLoading: false,
98-
allUsers: false,
99+
allUsers: true,
99100
filtering: '',
100101
members: [],
101102
room: params.room || ({} as TSubscriptionModel),
@@ -123,38 +124,84 @@ const RoomMembersView = (): React.ReactElement => {
123124

124125
useEffect(() => {
125126
const subscription = params?.room?.observe && params.room.observe().subscribe(changes => updateState({ room: changes }));
126-
setHeader(false);
127-
fetchMembers(false);
127+
setHeader(true);
128128
return () => subscription?.unsubscribe();
129129
}, []);
130130

131+
const fetchRoles = () => {
132+
if (isGroupChat(state.room)) {
133+
return;
134+
}
135+
if (
136+
muteUserPermission ||
137+
setLeaderPermission ||
138+
setOwnerPermission ||
139+
setModeratorPermission ||
140+
removeUserPermission ||
141+
editTeamMemberPermission ||
142+
viewAllTeamChannelsPermission ||
143+
viewAllTeamsPermission
144+
) {
145+
fetchRoomMembersRoles(state.room.t as any, state.room.rid, updateState);
146+
}
147+
};
148+
149+
const fetchMembers = useCallback(async () => {
150+
const { members, isLoading, end, room, filter, page, allUsers } = state;
151+
const { t } = room;
152+
153+
if (isLoading || end) {
154+
return;
155+
}
156+
157+
const requestId = ++latestSearchRequest.current;
158+
updateState({ isLoading: true });
159+
160+
try {
161+
const membersResult = await getRoomMembers({
162+
rid: room.rid,
163+
roomType: t,
164+
type: allUsers ? 'all' : 'online',
165+
filter,
166+
skip: PAGE_SIZE * page,
167+
limit: PAGE_SIZE,
168+
allUsers
169+
});
170+
171+
if (requestId !== latestSearchRequest.current) {
172+
return;
173+
}
174+
175+
const existingIds = new Set(members.map(m => m._id));
176+
const membersResultFiltered = membersResult?.filter((member: TUserModel) => !existingIds.has(member._id));
177+
178+
// Safety check: if page is 0, we replace the list entirely
179+
const newMembers = page === 0 ? membersResultFiltered : [...members, ...(membersResultFiltered || [])];
180+
const isEnd = membersResult?.length < PAGE_SIZE;
181+
182+
updateState({
183+
members: newMembers,
184+
isLoading: false,
185+
end: isEnd,
186+
page: page + 1
187+
});
188+
} catch (e) {
189+
log(e);
190+
if (requestId === latestSearchRequest.current) {
191+
updateState({ isLoading: false });
192+
}
193+
}
194+
}, [state.isLoading, state.end, state.room.t, state.filter, state.page, state.allUsers]);
195+
131196
useEffect(() => {
132197
const unsubscribe = navigation.addListener('focus', () => {
133-
const { allUsers } = state;
134-
fetchMembers(allUsers);
198+
fetchMembers();
135199
});
136200

137201
return unsubscribe;
138202
}, [navigation]);
139203

140204
useEffect(() => {
141-
const fetchRoles = () => {
142-
if (isGroupChat(state.room)) {
143-
return;
144-
}
145-
if (
146-
muteUserPermission ||
147-
setLeaderPermission ||
148-
setOwnerPermission ||
149-
setModeratorPermission ||
150-
removeUserPermission ||
151-
editTeamMemberPermission ||
152-
viewAllTeamChannelsPermission ||
153-
viewAllTeamsPermission
154-
) {
155-
fetchRoomMembersRoles(state.room.t as any, state.room.rid, updateState);
156-
}
157-
};
158205
fetchRoles();
159206
}, [
160207
muteUserPermission,
@@ -164,13 +211,35 @@ const RoomMembersView = (): React.ReactElement => {
164211
removeUserPermission,
165212
editTeamMemberPermission,
166213
viewAllTeamChannelsPermission,
167-
viewAllTeamsPermission
214+
viewAllTeamsPermission,
215+
state.room?.rid,
216+
state.room?.t
168217
]);
169218

219+
useEffect(() => {
220+
fetchMembers();
221+
}, [state.filter, state.allUsers]);
222+
223+
const debounceFilterChange = useDebounce((text: string) => {
224+
const trimmedFilter = text.trim();
225+
226+
if (!trimmedFilter) {
227+
latestSearchRequest.current += 1;
228+
}
229+
230+
updateState({
231+
filter: trimmedFilter,
232+
page: 0,
233+
members: [],
234+
end: false,
235+
isLoading: false
236+
});
237+
}, 500);
238+
170239
const toggleStatus = (status: boolean) => {
171240
try {
172-
updateState({ members: [], allUsers: status, end: false });
173-
fetchMembers(status);
241+
// We only update 'allUsers'. 'filter' remains in state, so the next fetch uses both.
242+
updateState({ members: [], allUsers: status, end: false, page: 0 });
174243
setHeader(status);
175244
} catch (e) {
176245
log(e);
@@ -189,14 +258,14 @@ const RoomMembersView = (): React.ReactElement => {
189258
options: [
190259
{
191260
title: I18n.t('Online'),
192-
onPress: () => toggleStatus(true),
193-
right: () => <Radio check={allUsers} />,
261+
onPress: () => toggleStatus(false),
262+
right: () => <Radio check={!allUsers} />,
194263
testID: 'room-members-view-toggle-status-online'
195264
},
196265
{
197266
title: I18n.t('All'),
198-
onPress: () => toggleStatus(false),
199-
right: () => <Radio check={!allUsers} />,
267+
onPress: () => toggleStatus(true),
268+
right: () => <Radio check={allUsers} />,
200269
testID: 'room-members-view-toggle-status-all'
201270
}
202271
]
@@ -348,49 +417,10 @@ const RoomMembersView = (): React.ReactElement => {
348417
});
349418
};
350419

351-
const fetchMembers = async (status: boolean) => {
352-
const { members, isLoading, end, room, filter, page } = state;
353-
const { t } = room;
354-
355-
if (isLoading || end) {
356-
return;
357-
}
358-
359-
updateState({ isLoading: true });
360-
try {
361-
const membersResult = await getRoomMembers({
362-
rid: room.rid,
363-
roomType: t,
364-
type: !status ? 'all' : 'online',
365-
filter,
366-
skip: PAGE_SIZE * page,
367-
limit: PAGE_SIZE,
368-
allUsers: !status
369-
});
370-
const end = membersResult?.length < PAGE_SIZE;
371-
const membersResultFiltered = membersResult?.filter((member: TUserModel) => !members.some(m => m._id === member._id));
372-
updateState({
373-
members: [...members, ...membersResultFiltered],
374-
isLoading: false,
375-
end,
376-
page: page + 1
377-
});
378-
} catch (e) {
379-
log(e);
380-
updateState({ isLoading: false });
381-
}
382-
};
383-
384-
const filter = sanitizeLikeString(state.filter.toLowerCase()) || '';
385-
const filteredMembers =
386-
state.members && state.members.length > 0 && state.filter
387-
? state.members.filter(m => m.username.toLowerCase().match(filter) || m.name?.toLowerCase().match(filter))
388-
: null;
389-
390420
return (
391421
<SafeAreaView testID='room-members-view'>
392422
<FlatList
393-
data={filteredMembers || state.members}
423+
data={state.members}
394424
renderItem={({ item }) => (
395425
<View style={{ backgroundColor: colors.surfaceRoom }}>
396426
<UserItem
@@ -412,12 +442,12 @@ const RoomMembersView = (): React.ReactElement => {
412442
t={state.room.t}
413443
abacAttributes={state.room.abacAttributes}
414444
/>
415-
<SearchBox onChangeText={text => updateState({ filter: text.trim() })} testID='room-members-view-search' />
445+
<SearchBox onChangeText={text => debounceFilterChange(text)} testID='room-members-view-search' />
416446
</>
417447
}
418448
ListFooterComponent={() => (state.isLoading ? <ActivityIndicator /> : null)}
419449
onEndReachedThreshold={0.1}
420-
onEndReached={() => fetchMembers(state.allUsers)}
450+
onEndReached={() => fetchMembers()}
421451
ListEmptyComponent={() =>
422452
state.end ? (
423453
<Text style={[styles.noResult, { color: colors.fontTitlesLabels }]}>{I18n.t('No_members_found')}</Text>

0 commit comments

Comments
 (0)