Skip to content

Conversation

@Rohit3523
Copy link
Contributor

@Rohit3523 Rohit3523 commented Jan 21, 2026

Proposed changes

Search functionality in the room/group members list fails to return results for users who haven’t been loaded into the local state. Only users loaded initially or after scrolling to the bottom are searchable.

Issue(s)

https://rocketchat.atlassian.net/browse/SUP-973

How to test or reproduce

  1. Go to the member search in any room with a large number of members.
  2. Type the name of a user who is a member of the room but not currently visible in the list.
  3. The app fails to display the member in the search results.

Screenshots

iOS E2E
Screenshot 2026-01-23 at 11 40 54 PM

Android E2E
Screenshot 2026-01-23 at 11 23 05 PM

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • Improvement (non-breaking change which improves a current function)
  • New feature (non-breaking change which adds functionality)
  • Documentation update (if none of the other choices apply)

Checklist

  • I have read the CONTRIBUTING doc
  • I have signed the CLA
  • Lint and unit tests pass locally with my changes
  • I have added tests that prove my fix is effective or that my feature works (if applicable)
  • I have added necessary documentation (if applicable)
  • Any dependent changes have been merged and published in downstream modules

Further comments

Summary by CodeRabbit

  • Improvements

    • Debounced member search (500ms) for smoother typing and fewer requests.
    • Member list now relies on server-filtered results as the single source of truth.
    • Pagination deduplicates results and preserves correct end/page behavior.
    • Stale search responses are ignored to prevent flicker or incorrect lists.
    • Search input integrated into the list header for consistent behavior.
  • Tests

    • Added an automated end-to-end test for the "Search Member" flow.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

Walkthrough

Adds a 500ms debounced search to RoomMembersView that resets pagination on new queries, issues server-side filtered fetches, tracks request IDs to ignore stale responses, deduplicates incoming members into state, and removes client-side sanitizeLikeString filtering.

Changes

Cohort / File(s) Summary
Room members view
app/views/RoomMembersView/index.tsx
Added useRef import and useDebounce usage; changed initial allUsers state; set header on mount and fetch on focus.
Debounced search & filter handling
app/views/RoomMembersView/index.tsx
Introduced latestSearchRequest ref and debounceFilterChange (500ms) to trim input, reset page/members/end, and trigger filtered fetches from the server. SearchBox now uses the debounced handler.
Fetch, pagination & deduplication
app/views/RoomMembersView/index.tsx
fetchMembers signature simplified (no status param); requests use requestId guard to ignore stale responses; incoming members deduplicated against state.members; preserves page/end logic; errors clear loading only when not stale.
UI wiring & state changes
app/views/RoomMembersView/index.tsx
Removed client-side text filtering and sanitizeLikeString usage; onEndReached calls fetchMembers(); list renders from state.members; status toggles reset pagination.
Automated test
.maestro/tests/room/search-member.yaml
New Maestro test "Search Member": creates a user, logs in, opens room members, inputs rohit.bansal, and asserts member visibility.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant SB as SearchBox
  participant RMV as RoomMembersView
  participant API as ServerAPI

  User->>SB: type query
  SB->>RMV: onChangeText (debounced 500ms)
  rect rgba(200,200,255,0.5)
    RMV->>RMV: latestSearchRequest++\nreset page, members, end
  end
  RMV->>API: fetchMembers(searchFilter, requestId)
  API-->>RMV: response (members, page, requestId)
  alt response.requestId == latestSearchRequest
    rect rgba(200,255,200,0.5)
      RMV->>RMV: deduplicate & merge members\nupdate page & end\nclear loading
    end
  else stale response
    rect rgba(255,200,200,0.5)
      RMV-->>RMV: ignore response
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • diegolmello
  • OtavioStasiak

Poem

🐇 I pause a beat and twitch my ear,
A half-second hush, then results appear.
I hop through pages, stitch new faces in,
Drop stale echoes, keep the list so thin. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: shifting room member search from local client-side filtering to server-side data.
Linked Issues check ✅ Passed The changes implement server-side member search by removing client-side filtering and using state-driven fetch logic, addressing the core requirement of SUP-973.
Out of Scope Changes check ✅ Passed All changes in RoomMembersView and the new test file are directly related to implementing server-side member search; no unrelated modifications detected.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch search-filter-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
app/views/RoomMembersView/index.tsx (2)

44-44: Refetch on cleared search + use sanitizeLikeString (fixes lint).
Line 44 currently fails lint, and when the filter is cleared the list is reset but never reloaded. Normalize the filter and always fetch so the list repopulates and the unused import is resolved.

✅ Proposed fix
- const handleFilterChange = (text: string) => {
- 	const trimmedFilter = text.trim();
+ const handleFilterChange = (text: string) => {
+ 	const normalizedFilter = sanitizeLikeString(text.trim());
 
 	updateState({
-		filter: trimmedFilter,
+		filter: normalizedFilter,
 		page: 0,
 		members: [],
 		end: false
 	});
 
-	if (trimmedFilter.length > 0) {
-		fetchMembersWithNewFilter(trimmedFilter);
-	}
+	fetchMembersWithNewFilter(normalizedFilter);
 };

Also applies to: 425-437


440-468: Use stop() instead of cancel() for debounce cleanup, and remove the redundant filteredMembers variable.

The debounce helper exposes a stop() method (not cancel()), and filteredMembers is redundant—it always equals state.members because the condition state.members.length > 0 ? state.members : null followed by filteredMembers || state.members always evaluates to state.members.

The memoization pattern is correct to prevent recreating the debounced function per render and avoid orphaned callbacks after unmount.

♻️ Corrected refactor
-import React, { useEffect, useReducer } from 'react';
+import React, { useEffect, useReducer, useCallback, useMemo } from 'react';

-const handleFilterChange = (text: string) => {
+const handleFilterChange = useCallback((text: string) => {
 	// existing body
-};
+}, [fetchMembersWithNewFilter]);

-const debounceFilterChange = debounce(handleFilterChange, 500);
-const filteredMembers = state.members.length > 0 ? state.members : null;
+const debounceFilterChange = useMemo(() => debounce(handleFilterChange, 500), [handleFilterChange]);
+useEffect(() => () => debounceFilterChange.stop?.(), [debounceFilterChange]);

And update the FlatList data prop from filteredMembers || state.members to simply state.members.

🤖 Fix all issues with AI agents
In `@app/views/RoomMembersView/index.tsx`:
- Around line 371-381: When filter is active the code assigns newMembers =
membersResult which replaces prior pages and breaks scrolling; update the logic
in the block that references filter, membersResult, newMembers and members so
that when filter is truthy you either (A) append membersResult to existing
members with de-duplication by _id (e.g. merge members + membersResult and
remove duplicates by _id) to preserve pagination, or (B) explicitly disable
pagination while filtered (set end = true or prevent page increments) so
filtered requests don't replace earlier pages—apply the chosen approach
consistently where PAGE_SIZE, membersResult, newMembers and members are used.
- Around line 395-418: The fetchMembersWithNewFilter function can have
out-of-order debounced responses overwrite newer results; add a request-tracking
guard (e.g., increment a requestId counter or store the latestFilter in
component state) before calling getRoomMembers and capture the current
id/filter, then when the response returns only call updateState(members...) if
the captured id/filter still matches the latest one in state; ensure
requestId/latestFilter is updated at the start of fetchMembersWithNewFilter and
used to ignore stale responses so members/end/page/isLoading updates come only
from the most recent request.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@app/views/RoomMembersView/index.tsx`:
- Line 447: debounceFilterChange is recreated every render which breaks
debouncing; memoize the debounced function instead of creating it in the
component body by wrapping the debounce(handleFilterChange, 500) call in useMemo
(or store it in a useRef) so the same debounced instance persists across
renders; if handleFilterChange needs stable identity or fresh state, make
handleFilterChange a useCallback with appropriate deps or keep a ref to the
latest handler and call that from the debounced function.
- Around line 432-445: handleFilterChange currently clears members and resets
state but only calls fetchMembersWithNewFilter when trimmedFilter.length > 0,
leaving the list empty when the user clears the search; modify
handleFilterChange so that when trimmedFilter is empty it also triggers the
unfiltered fetch (e.g., call fetchMembersWithNewFilter('') or a dedicated
fetchUnfilteredMembers function) after updateState, keeping the page reset and
end flag behavior intact; reference the handleFilterChange function,
trimmedFilter variable, updateState call, and fetchMembersWithNewFilter to
locate and change the logic.
♻️ Duplicate comments (1)
app/views/RoomMembersView/index.tsx (1)

374-380: Filtered pagination still drops earlier pages.

When a filter is active and the user scrolls to load more, fetchMembers is triggered via onEndReached. The current logic replaces members with just the new page instead of appending. Consider either appending with de-duplication for filtered paging, or disabling onEndReached while a filter is active.

🐛 Proposed fix: Append with de-dupe for all cases
-		let newMembers: TUserModel[];
-		if (filter) {
-			newMembers = membersResult || [];
-		} else {
-			const membersResultFiltered = membersResult?.filter((member: TUserModel) => !members.some(m => m._id === member._id));
-			newMembers = [...members, ...membersResultFiltered];
-		}
+		const membersResultFiltered = membersResult?.filter((member: TUserModel) => !members.some(m => m._id === member._id));
+		const newMembers = [...members, ...(membersResultFiltered || [])];
🧹 Nitpick comments (1)
app/views/RoomMembersView/index.tsx (1)

448-448: Redundant filteredMembers variable.

The expression filteredMembers || state.members always evaluates to state.members:

  • If state.members.length > 0: filteredMembers = state.members
  • If state.members.length === 0: filteredMembers = null, fallback = state.members (empty array)
♻️ Simplify by using state.members directly
-const filteredMembers = state.members.length > 0 ? state.members : null;
 ...
-data={filteredMembers || state.members}
+data={state.members}

Also applies to: 453-453

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/views/RoomMembersView/index.tsx (1)

352-386: Prevent stale pagination responses from mixing with a new search.
If a page fetch started before a filter/status change returns late, it can append unfiltered members into the filtered list because fetchMembers has no staleness guard. Add a shared request token (or query signature ref) so only the latest request updates state.

🐛 Proposed fix (shared request guard)
@@
-const latestSearchRequest = useRef(0);
+const latestSearchRequest = useRef(0);
+const latestMembersRequest = useRef(0);
+const latestQueryRef = useRef({ filter: '', allUsers: false });
+
+useEffect(() => {
+	latestQueryRef.current = { filter: state.filter, allUsers: state.allUsers };
+}, [state.filter, state.allUsers]);
@@
 const fetchMembers = async (status: boolean) => {
+	const requestId = ++latestMembersRequest.current;
+	const filterAtCall = state.filter;
+	const allUsersAtCall = status;
 	const { members, isLoading, end, room, filter, page } = state;
@@
 	try {
 		const membersResult = await getRoomMembers({
@@
 		const end = membersResult?.length < PAGE_SIZE;
 
 		const membersResultFiltered = membersResult?.filter((member: TUserModel) => !members.some(m => m._id === member._id));
 		const newMembers = [...members, ...(membersResultFiltered || [])];
 
+		if (
+			requestId !== latestMembersRequest.current ||
+			filterAtCall !== latestQueryRef.current.filter ||
+			allUsersAtCall !== latestQueryRef.current.allUsers
+		) {
+			return;
+		}
+
 		updateState({
 			members: newMembers,
 			isLoading: false,
 			end,
 			page: page + 1
 		});
 	} catch (e) {
 		log(e);
-		updateState({ isLoading: false });
+		if (requestId === latestMembersRequest.current) {
+			updateState({ isLoading: false });
+		}
 	}
 };
♻️ Duplicate comments (1)
app/views/RoomMembersView/index.tsx (1)

442-470: Debounced handler is recreated every render (debounce breaks + stale timers).
Line 442 creates a new debounce instance each render, so timeouts from prior renders can still fire and the debounce no longer coalesces rapid input. Memoize the debounced function and clean it up on unmount.

🐛 Proposed fix (stable debounce + cleanup)
-import React, { useEffect, useReducer, useRef } from 'react';
+import React, { useEffect, useMemo, useReducer, useRef } from 'react';
@@
-const debounceFilterChange = debounce(handleFilterChange, 500);
+const handleFilterChangeRef = useRef(handleFilterChange);
+useEffect(() => {
+	handleFilterChangeRef.current = handleFilterChange;
+}, [handleFilterChange]);
+
+const debounceFilterChange = useMemo(
+	() => debounce((text: string) => handleFilterChangeRef.current(text), 500),
+	[]
+);
+useEffect(() => () => debounceFilterChange.stop(), [debounceFilterChange]);

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/views/RoomMembersView/index.tsx`:
- Around line 171-184: The filter debounce handler (debounceFilterChange) and
fetchMembersWithNewFilter call flow can be clobbered by in-flight fetchMembers
responses because there’s no shared request-id guard; add a single shared
request identifier (e.g., currentRequestId) used by both fetchMembers and
fetchMembersWithNewFilter so every fetch captures the id before dispatch and
each response checks it and ignores stale ids, and when the filter is cleared
(trimmedFilter.length === 0) increment/invalidate the currentRequestId so any
pending filtered responses are ignored; updateState calls remain the same but
ensure all code paths that call fetchMembers or fetchMembersWithNewFilter use
that guard to prevent stale overwrites.
🧹 Nitpick comments (1)
app/views/RoomMembersView/index.tsx (1)

442-447: Simplify redundant filteredMembers logic.

filteredMembers is either state.members or null, and data={filteredMembers || state.members} always resolves to state.members. You can remove the intermediate variable.

♻️ Suggested simplification
-const filteredMembers = state.members.length > 0 ? state.members : null;
...
-	data={filteredMembers || state.members}
+	data={state.members}

@Rohit3523
Copy link
Contributor Author

@coderabbitai review

@Rohit3523 Rohit3523 had a problem deploying to experimental_android_build January 23, 2026 17:28 — with GitHub Actions Error
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/views/RoomMembersView/index.tsx (1)

454-459: Search becomes a no‑op on servers < 3.16 because the fallback API ignores the filter parameter.

The issue is confirmed: getRoomMembers only applies the filter parameter on servers >= 3.16.0; the legacy getUsersOfRoom path doesn't accept it at all. With no client-side filtering fallback, the SearchBox won't filter results on older servers.

Current filtering flow

Both fetchMembers() and fetchMembersWithNewFilter() pass the filter parameter to getRoomMembers(), which:

  • For servers ≥ 3.16.0: Uses {roomType}.members API with the filter applied
  • For servers < 3.16.0: Falls back to getUsersOfRoom() method call, passing only rid, allUsers, and pagination—filter is silently dropped

The FlatList then displays state.members directly with no client-side filtering.

Consider implementing client-side filtering (or disabling search) when the server doesn't support the filter parameter, similar to the proposed solution. If the app's minimum supported server version is 3.16 or higher, document and enforce that requirement instead.

@Rohit3523 Rohit3523 requested a review from diegolmello January 23, 2026 17:34
@Rohit3523 Rohit3523 deployed to approve_e2e_testing January 23, 2026 18:15 — with GitHub Actions Active
@Rohit3523 Rohit3523 had a problem deploying to official_android_build January 23, 2026 18:18 — with GitHub Actions Error
@Rohit3523 Rohit3523 had a problem deploying to experimental_android_build January 23, 2026 18:18 — with GitHub Actions Error
@Rohit3523 Rohit3523 had a problem deploying to experimental_ios_build January 23, 2026 18:18 — with GitHub Actions Error
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @.maestro/tests/room/search-member.yaml:
- Around line 35-39: The test uses a hardcoded username ("rohit.bansal") in
inputText and waits for id 'room-members-view-item-rohit.bansal', creating an
external dependency; instead, create a second test user at runtime via
output.utils.createUser() (as in unread-badge.yaml), assign the returned
username to the inputText and update the extendedWaitUntil id to use that
dynamic username (replace 'room-members-view-item-rohit.bansal' with the
generated user's identifier) so the test is self-contained and no longer depends
on an existing external user.

Copy link
Contributor

@OtavioStasiak OtavioStasiak left a comment

Choose a reason for hiding this comment

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

I've tested this against Prod and Web behaviors and found a failing case.

Scenario: If you search for a user and then switch the filter to 'Online', the app loads the entire list of online users, ignoring the current search term.

Expected behavior: It should apply the status filter to the current search results (matching both the name and the status).

state.members && state.members.length > 0 && state.filter
? state.members.filter(m => m.username.toLowerCase().match(filter) || m.name?.toLowerCase().match(filter))
: null;
const fetchMembersWithNewFilter = async (searchFilter: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We have two functions that do almost the same thing... can't we unify it?
Example:

	const loadMembers = async ({
		filterParam,
		allUsersParam,
		reset = false
	}: {
		filterParam?: string;
		allUsersParam?: boolean;
		reset?: boolean;
	} = {}) => {
		const { members, isLoading, end, room, filter, page, allUsers } = state;
		
		// Determine values to use (param override -> state -> default)
		const currentFilter = filterParam !== undefined ? filterParam : filter;
		const currentAllUsers = allUsersParam !== undefined ? allUsersParam : allUsers;

		// Prevent fetching if loading or reached end (unless it's a reset/new search)
		if (!reset && (isLoading || end)) {
			return;
		}

		const requestId = ++latestSearchRequest.current;

		// Optimistic State Updates
		if (reset) {
			updateState({
				isLoading: true,
				members: [],
				end: false,
				filter: currentFilter,
				allUsers: currentAllUsers,
				page: 0
			});
		} else {
			updateState({ isLoading: true });
		}

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

			// Race Condition Check
			if (requestId !== latestSearchRequest.current) {
				return;
			}

			const isEnd = (membersResult?.length ?? 0) < PAGE_SIZE;
			let newMembers = membersResult || [];

			// If appending (not resetting), filter out duplicates
			if (!reset) {
				const filteredNewMembers = newMembers.filter(member => !members.some(m => m._id === member._id));
				newMembers = [...members, ...filteredNewMembers];
			}

			updateState({
				members: newMembers,
				isLoading: false,
				end: isEnd,
				page: reset ? 1 : page + 1
			});
		} catch (e) {
			log(e);
			if (requestId === latestSearchRequest.current) {
				updateState({ isLoading: false });
			}
		}
	};

- 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@app/views/RoomMembersView/index.tsx`:
- Around line 171-189: The debounced callback debounceFilterChange captures a
stale fetchMembersWithNewFilter because useDebounce/useDebouncedCallback has no
dependency array; fix by making fetchMembersWithNewFilter stable with
useCallback and include all values it reads (room, allUsers, any state setters
like updateState) in its dependency list so the debounced callback always calls
the current logic; ensure latestSearchRequest.current usage remains as-is to
discard responses and that debounceFilterChange continues to call the memoized
fetchMembersWithNewFilter.
🧹 Nitpick comments (1)
app/views/RoomMembersView/index.tsx (1)

416-452: Consider unifying fetch functions.

Both fetchMembers and fetchMembersWithNewFilter share significant logic (request ID tracking, error handling, API call structure). As noted in a previous review comment, these could be consolidated into a single loadMembers function with a reset parameter to reduce duplication and simplify maintenance.

This is an optional refactor that can be deferred.

@Rohit3523 Rohit3523 requested a deployment to approve_e2e_testing January 26, 2026 13:44 — with GitHub Actions Waiting
@Rohit3523 Rohit3523 requested a deployment to experimental_android_build January 26, 2026 13:46 — with GitHub Actions Waiting
@Rohit3523 Rohit3523 requested a deployment to experimental_ios_build January 26, 2026 13:46 — with GitHub Actions Waiting
@Rohit3523 Rohit3523 requested a deployment to official_android_build January 26, 2026 13:46 — with GitHub Actions Waiting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants