Skip to content

feat(search): validate mxid profile via API before displaying external contact#2918

Merged
hoangdat merged 1 commit intomainfrom
feature/TW-2474-improve-searching-non-exist-matrix-address
Mar 27, 2026
Merged

feat(search): validate mxid profile via API before displaying external contact#2918
hoangdat merged 1 commit intomainfrom
feature/TW-2474-improve-searching-non-exist-matrix-address

Conversation

@9clg6
Copy link
Copy Markdown
Collaborator

@9clg6 9clg6 commented Mar 16, 2026

Ticket

TID-2474 - Improve searching by the non-exist matrix address

Root cause

SearchExternalContactWidget displayed any syntactically valid mxid as a contact result without verifying it exists on the server. The check was purely format-based (isValidMatrixId), so typing @noexist:server.com showed a fake profile instead of "No Results".

Solution

Converted SearchExternalContactWidget from StatelessWidget to StatefulWidget with a FutureBuilder that calls client.getProfileFromUserId() before rendering:

  • 200 → display the contact with real profile data
  • 404/403/5xx → display EmptySearchWidget ("No Results")
  • Loading → display a spinner

Impact description

N/A (bug fix)

Test recommendations

  1. Search @validuser:your-server.com → should show profile info
  2. Search @nonexistent:your-server.com → should show "No Results" mascot

Pre-merge

No

Resolved

  • Web:
image
  • Android:
  • IOS:
test.mp4

Summary by CodeRabbit

  • New Features
    • External contact search now fetches remote profile data, shows a loading indicator, and accepts an optional test client for testing.
  • Bug Fixes
    • Shows an explicit empty/no-results state when profile lookup fails or returns no data.
  • Improvements
    • UI updates responsively when query/ID changes and displays validated contact details; taps use fetched profile info.
  • Tests
    • Added widget and integration tests covering loading, no-result, and successful profile fetch flows.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 16, 2026

Walkthrough

Search-related widgets were converted from StatelessWidget to StatefulWidget and now fetch external Matrix profile information asynchronously via a Matrix client (injectable via a test-only parameter). Lifecycle methods (didChangeDependencies, didUpdateWidget) trigger profile fetches when inputs change. UI uses FutureBuilder to render loading, error/empty, or populated contact views, builds PresentationContact from fetched profile data (falling back to keyword-derived display name), and forwards the resolved contact on taps. New widget and integration tests cover loading, not-found, and success scenarios.

Suggested reviewers

  • dab246
  • tddang-linagora
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: validating matrix IDs via API before displaying external contacts, which is the core objective of the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed The PR description follows the required template with all key sections present: Ticket, Root cause, Solution, Impact description, Test recommendations, Pre-merge, and Resolved with screenshots.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/TW-2474-improve-searching-non-exist-matrix-address

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.

@github-actions
Copy link
Copy Markdown
Contributor

This PR has been deployed to https://linagora.github.io/twake-on-matrix/2918

@9clg6 9clg6 marked this pull request as draft March 16, 2026 13:24
@9clg6 9clg6 force-pushed the feature/TW-2474-improve-searching-non-exist-matrix-address branch from 577e116 to adcde8a Compare March 16, 2026 13:55
@9clg6 9clg6 marked this pull request as ready for review March 16, 2026 13:56
Copy link
Copy Markdown

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

🧹 Nitpick comments (1)
lib/pages/search/search_external_contact.dart (1)

60-72: FutureBuilder implementation correctly handles all connection states.

The three-state pattern is sound. Consider adding error logging to help debug issues in production:

📝 Optional: Add error logging for debugging
         if (snapshot.hasError || !snapshot.hasData) {
+          if (snapshot.hasError) {
+            Logs().v(
+              'SearchExternalContactWidget: Failed to fetch profile for ${widget.keyword}',
+              snapshot.error,
+            );
+          }
           return const EmptySearchWidget();
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/search/search_external_contact.dart` around lines 60 - 72, The
FutureBuilder in SearchExternalContact (using _profileFuture and the snapshot in
the builder) currently returns EmptySearchWidget when snapshot.hasError or
!snapshot.hasData; add an error logging call inside that error branch to record
snapshot.error and snapshot.stackTrace (or snapshot.error.toString()) with a
clear context message before returning the EmptySearchWidget so production logs
capture the failure details for debugging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/pages/search/search_external_contact.dart`:
- Around line 60-72: The FutureBuilder in SearchExternalContact (using
_profileFuture and the snapshot in the builder) currently returns
EmptySearchWidget when snapshot.hasError or !snapshot.hasData; add an error
logging call inside that error branch to record snapshot.error and
snapshot.stackTrace (or snapshot.error.toString()) with a clear context message
before returning the EmptySearchWidget so production logs capture the failure
details for debugging.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e424ddb7-847d-433b-8a12-e94d305f6289

📥 Commits

Reviewing files that changed from the base of the PR and between 577e116 and adcde8a.

📒 Files selected for processing (1)
  • lib/pages/search/search_external_contact.dart

@tddang-linagora
Copy link
Copy Markdown
Collaborator

Search @validuser:your-server.com → should show profile info
Search @Nonexistent:your-server.com → should show "No Results" mascot

This is your test recommendations in the PR description, but the current demo is no where near any of them. Please update the demo accordingly.

@9clg6 9clg6 force-pushed the feature/TW-2474-improve-searching-non-exist-matrix-address branch from adcde8a to 1b4e02b Compare March 17, 2026 10:41
Copy link
Copy Markdown

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

🧹 Nitpick comments (1)
test/pages/search/search_external_contact_test.dart (1)

149-217: Comprehensive test coverage for all three states.

The tests correctly validate:

  1. Loading: Uses a never-completing Completer to keep the future pending
  2. Error: Simulates M_NOT_FOUND via MatrixException.fromJson
  3. Success: Verifies profile displayName is rendered

Missing test case: The implementation at line 80 uses profile.displayname ?? widget.keyword.substring(1) as a fallback when displayname is null. Add a test to verify this fallback behavior—when getUserProfile returns a valid profile with displayname: null, the tile should display the keyword (minus the "@" prefix).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/pages/search/search_external_contact_test.dart` around lines 149 - 217,
Add a test that stubs mockClient.getUserProfile (used in buildWidget) to return
a CachedProfileInformation.fromProfile where ProfileInformation has displayname:
null; call buildWidget with keyword '@validuser:server.com', pumpAndSettle, then
assert ExpansionContactListTile is shown and that the rendered text contains the
fallback keyword without the leading '@' (i.e., widget.keyword.substring(1) ->
'validuser:server.com'), verifying the profile.displayname ??
widget.keyword.substring(1) fallback path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@test/pages/search/search_external_contact_test.dart`:
- Around line 149-217: Add a test that stubs mockClient.getUserProfile (used in
buildWidget) to return a CachedProfileInformation.fromProfile where
ProfileInformation has displayname: null; call buildWidget with keyword
'@validuser:server.com', pumpAndSettle, then assert ExpansionContactListTile is
shown and that the rendered text contains the fallback keyword without the
leading '@' (i.e., widget.keyword.substring(1) -> 'validuser:server.com'),
verifying the profile.displayname ?? widget.keyword.substring(1) fallback path.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1a6d19ba-2fd1-4a6f-b9d7-39eb2bda9803

📥 Commits

Reviewing files that changed from the base of the PR and between adcde8a and 1b4e02b.

📒 Files selected for processing (4)
  • integration_test/tests/chat/search_external_mxid_test.dart
  • lib/pages/contacts_tab/contacts_tab_body_view.dart
  • lib/pages/search/search_external_contact.dart
  • test/pages/search/search_external_contact_test.dart

@9clg6 9clg6 force-pushed the feature/TW-2474-improve-searching-non-exist-matrix-address branch from 1b4e02b to 92e6932 Compare March 17, 2026 10:53
@9clg6 9clg6 force-pushed the feature/TW-2474-improve-searching-non-exist-matrix-address branch from 92e6932 to 4732672 Compare March 17, 2026 10:58
Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
test/pages/search/search_external_contact_test.dart (1)

64-109: Stabilize GetIt state per test case.

New mocks are created in every setUp, but singleton registration is skipped after first test due isRegistered, which can leak DI state and stubs across cases. Prefer resetting GetIt and re-registering dependencies per test.

Suggested fix
-  setUp(() {
+  setUp(() async {
     final getIt = GetIt.instance;
+    await getIt.reset();
@@
-    if (!getIt.isRegistered<HiveGetInvitationStatusInteractor>()) {
-      getIt.registerSingleton<HiveGetInvitationStatusInteractor>(
-        MockHiveGetInvitationStatusInteractor(),
-      );
-    }
-    if (!getIt.isRegistered<GetInvitationStatusInteractor>()) {
-      getIt.registerSingleton<GetInvitationStatusInteractor>(
-        MockGetInvitationStatusInteractor(),
-      );
-    }
-    if (!getIt.isRegistered<PostAddressBookInteractor>()) {
-      getIt.registerSingleton<PostAddressBookInteractor>(
-        MockPostAddressBookInteractor(),
-      );
-    }
-    if (!getIt.isRegistered<HiveDeleteInvitationStatusInteractor>()) {
-      getIt.registerSingleton<HiveDeleteInvitationStatusInteractor>(
-        MockHiveDeleteInvitationStatusInteractor(),
-      );
-    }
-    if (!getIt.isRegistered<DeleteThirdPartyContactBoxInteractor>()) {
-      getIt.registerSingleton<DeleteThirdPartyContactBoxInteractor>(
-        MockDeleteThirdPartyContactBoxInteractor(),
-      );
-    }
+    getIt.registerSingleton<HiveGetInvitationStatusInteractor>(
+      MockHiveGetInvitationStatusInteractor(),
+    );
+    getIt.registerSingleton<GetInvitationStatusInteractor>(
+      MockGetInvitationStatusInteractor(),
+    );
+    getIt.registerSingleton<PostAddressBookInteractor>(
+      MockPostAddressBookInteractor(),
+    );
+    getIt.registerSingleton<HiveDeleteInvitationStatusInteractor>(
+      MockHiveDeleteInvitationStatusInteractor(),
+    );
+    getIt.registerSingleton<DeleteThirdPartyContactBoxInteractor>(
+      MockDeleteThirdPartyContactBoxInteractor(),
+    );
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/pages/search/search_external_contact_test.dart` around lines 64 - 109,
Reset the GetIt container at the start of each setUp and register all required
singletons unconditionally there (instead of using setUpAll and checking
isRegistered), e.g., call GetIt.instance.reset() (or GetIt.I.reset()) at the top
of setUp, then register ResponsiveUtils, HiveGetInvitationStatusInteractor,
GetInvitationStatusInteractor, PostAddressBookInteractor,
HiveDeleteInvitationStatusInteractor, DeleteThirdPartyContactBoxInteractor and
any other mocks via registerSingleton so each test starts with a fresh DI state
and no leaked stubs from previous tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/pages/contacts_tab/contacts_tab_body_view.dart`:
- Around line 303-311: When widget.externalContact.matrixId is null, the method
_fetchProfileIfNeeded currently leaves previous values intact causing stale UI;
update _fetchProfileIfNeeded to handle the null branch by clearing
_currentMatrixId and _profileFuture (set them to null) so any previous profile
future is discarded; reference the symbols _fetchProfileIfNeeded,
_currentMatrixId, _profileFuture and widget.externalContact.matrixId and ensure
the clearing happens in the else/null case so the view will stop rendering old
profile data.

In `@lib/pages/search/search_external_contact.dart`:
- Around line 49-57: The _fetchProfileIfNeeded method only compares
_currentKeyword to widget.keyword, so it fails to refetch when the active client
changes; update the invalidation logic to also track and compare the current
client (use widget.clientForTesting ?? Matrix.of(context).client) against a
stored field (e.g., _currentClient) and when either the keyword or client
identity differs, set _currentKeyword and _currentClient and call
client.getUserProfile(...) to assign _profileFuture (preserving maxCacheAge:
Duration.zero and existing behavior in _fetchProfileIfNeeded).

---

Nitpick comments:
In `@test/pages/search/search_external_contact_test.dart`:
- Around line 64-109: Reset the GetIt container at the start of each setUp and
register all required singletons unconditionally there (instead of using
setUpAll and checking isRegistered), e.g., call GetIt.instance.reset() (or
GetIt.I.reset()) at the top of setUp, then register ResponsiveUtils,
HiveGetInvitationStatusInteractor, GetInvitationStatusInteractor,
PostAddressBookInteractor, HiveDeleteInvitationStatusInteractor,
DeleteThirdPartyContactBoxInteractor and any other mocks via registerSingleton
so each test starts with a fresh DI state and no leaked stubs from previous
tests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9718904a-55eb-4adf-8578-00daac1436aa

📥 Commits

Reviewing files that changed from the base of the PR and between 1b4e02b and 92e6932.

📒 Files selected for processing (4)
  • integration_test/tests/chat/search_external_mxid_test.dart
  • lib/pages/contacts_tab/contacts_tab_body_view.dart
  • lib/pages/search/search_external_contact.dart
  • test/pages/search/search_external_contact_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • integration_test/tests/chat/search_external_mxid_test.dart

@9clg6 9clg6 force-pushed the feature/TW-2474-improve-searching-non-exist-matrix-address branch from 4732672 to 6e4e557 Compare March 17, 2026 13:46
Copy link
Copy Markdown

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

🧹 Nitpick comments (1)
lib/pages/search/search_external_contact.dart (1)

62-74: Consider handling the null _profileFuture state more explicitly.

When _profileFuture is null (e.g., before the first didChangeDependencies call or if no keyword is provided), FutureBuilder treats it as having no data, which will show EmptySearchWidget. This may cause a brief flash of "No Results" before the actual loading state appears.

If this is intentional, it's fine. Otherwise, consider adding an explicit null check:

💡 Optional: Handle null future explicitly
   Widget build(BuildContext context) {
+    if (_profileFuture == null) {
+      return const SizedBox.shrink();
+    }
     return FutureBuilder<CachedProfileInformation>(
       future: _profileFuture,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/search/search_external_contact.dart` around lines 62 - 74, The
FutureBuilder is fed a possibly null _profileFuture which causes a brief flash
of EmptySearchWidget; update the widget to explicitly handle null futures by
checking _profileFuture before returning FutureBuilder (or inside the builder
check for snapshot.connectionState == ConnectionState.none or _profileFuture ==
null) and return an appropriate initial placeholder (e.g., SizedBox/empty
container or a different idle widget) instead of EmptySearchWidget; look for
_profileFuture, FutureBuilder<CachedProfileInformation>, EmptySearchWidget and
didChangeDependencies to locate where to add the null check and return the idle
placeholder until a real future is assigned.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@lib/pages/search/search_external_contact.dart`:
- Around line 62-74: The FutureBuilder is fed a possibly null _profileFuture
which causes a brief flash of EmptySearchWidget; update the widget to explicitly
handle null futures by checking _profileFuture before returning FutureBuilder
(or inside the builder check for snapshot.connectionState ==
ConnectionState.none or _profileFuture == null) and return an appropriate
initial placeholder (e.g., SizedBox/empty container or a different idle widget)
instead of EmptySearchWidget; look for _profileFuture,
FutureBuilder<CachedProfileInformation>, EmptySearchWidget and
didChangeDependencies to locate where to add the null check and return the idle
placeholder until a real future is assigned.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 45f3ae3c-68a3-4884-905a-a7837a263eb4

📥 Commits

Reviewing files that changed from the base of the PR and between 92e6932 and 6e4e557.

📒 Files selected for processing (4)
  • integration_test/tests/chat/search_external_mxid_test.dart
  • lib/pages/contacts_tab/contacts_tab_body_view.dart
  • lib/pages/search/search_external_contact.dart
  • test/pages/search/search_external_contact_test.dart

Copy link
Copy Markdown

@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

♻️ Duplicate comments (1)
lib/pages/search/search_external_contact.dart (1)

32-57: ⚠️ Potential issue | 🟠 Major

Invalidate the cached lookup when the client changes.

didUpdateWidget() only reruns the fetch when keyword changes, and _fetchProfileIfNeeded() keys _profileFuture only by keyword. Rebuilding this widget with the same MXID but a different clientForTesting/active Matrix client will keep the previous lookup alive.

Suggested fix
 class _SearchExternalContactWidgetState
     extends State<SearchExternalContactWidget> {
   Future<CachedProfileInformation>? _profileFuture;
   String? _currentKeyword;
+  Client? _currentClient;
@@
   `@override`
   void didUpdateWidget(covariant SearchExternalContactWidget oldWidget) {
     super.didUpdateWidget(oldWidget);
-    if (oldWidget.keyword != widget.keyword) {
-      _fetchProfileIfNeeded();
-    }
+    _fetchProfileIfNeeded();
   }
 
   void _fetchProfileIfNeeded() {
-    if (_currentKeyword != widget.keyword) {
-      _currentKeyword = widget.keyword;
-      final client = widget.clientForTesting ?? Matrix.of(context).client;
+    final client = widget.clientForTesting ?? Matrix.of(context).client;
+    if (_currentKeyword != widget.keyword ||
+        !identical(_currentClient, client)) {
+      _currentKeyword = widget.keyword;
+      _currentClient = client;
       _profileFuture = client.getUserProfile(
         widget.keyword,
         maxCacheAge: Duration.zero,
       );
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/search/search_external_contact.dart` around lines 32 - 57, The
widget currently only invalidates cached lookups by keyword; update
didUpdateWidget and _fetchProfileIfNeeded so the cache key includes the client
instance: when didUpdateWidget detects a different client (compare
oldWidget.clientForTesting ?? Matrix.of(context).client to
widget.clientForTesting ?? Matrix.of(context).client) force a refetch by
clearing/updating _currentKeyword or resetting _profileFuture, and in
_fetchProfileIfNeeded ensure you consider both widget.keyword and the resolved
client identity before returning the cached _profileFuture so a new client
triggers client.getUserProfile(...) even if the MXID didn't change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/pages/contacts_tab/contacts_tab_body_view.dart`:
- Around line 285-316: The profile fetch logic only keys off matrixId, so add
tracking of the Matrix client to force refetch when the controller's client
changes: add a field like _currentClient and in _fetchProfileIfNeeded (and/or in
didUpdateWidget) compare widget.controller.client to _currentClient; if they
differ, set _currentClient = widget.controller.client, reset _profileFuture (and
_currentMatrixId if needed) and call
widget.controller.client.getUserProfile(...) so the cached profile always
corresponds to the current controller.client used elsewhere (e.g.
onContactTap()).
- Around line 342-347: The code assigns profile.displayname directly to
PresentationContact.displayName which allows empty or whitespace names to
override the fallback; update the logic in the snapshot handling so you treat
blank/whitespace as missing: use profile.displayname trimmed and check for
null/empty (e.g. profile.displayname?.trim().isEmpty) and only use
profile.displayname (preferably trimmed) when non-empty, otherwise fall back to
widget.externalContact.displayName so PresentationContact (and downstream
onContactTap()) never gets a blank name.

---

Duplicate comments:
In `@lib/pages/search/search_external_contact.dart`:
- Around line 32-57: The widget currently only invalidates cached lookups by
keyword; update didUpdateWidget and _fetchProfileIfNeeded so the cache key
includes the client instance: when didUpdateWidget detects a different client
(compare oldWidget.clientForTesting ?? Matrix.of(context).client to
widget.clientForTesting ?? Matrix.of(context).client) force a refetch by
clearing/updating _currentKeyword or resetting _profileFuture, and in
_fetchProfileIfNeeded ensure you consider both widget.keyword and the resolved
client identity before returning the cached _profileFuture so a new client
triggers client.getUserProfile(...) even if the MXID didn't change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c9756aaa-6696-44a1-a920-4608b7fc0920

📥 Commits

Reviewing files that changed from the base of the PR and between 92e6932 and 6e4e557.

📒 Files selected for processing (4)
  • integration_test/tests/chat/search_external_mxid_test.dart
  • lib/pages/contacts_tab/contacts_tab_body_view.dart
  • lib/pages/search/search_external_contact.dart
  • test/pages/search/search_external_contact_test.dart
✅ Files skipped from review due to trivial changes (2)
  • test/pages/search/search_external_contact_test.dart
  • integration_test/tests/chat/search_external_mxid_test.dart

Copy link
Copy Markdown

@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

♻️ Duplicate comments (3)
lib/pages/contacts_tab/contacts_tab_body_view.dart (2)

345-346: ⚠️ Potential issue | 🟡 Minor

Ignore blank/whitespace profile.displayname when resolving display name.

A blank server display name currently overrides the fallback and can produce an empty label.

Suggested fix
-        final validatedContact = PresentationContact(
+        final resolvedDisplayName = profile.displayname?.trim();
+        final fallbackDisplayName = widget.externalContact.displayName?.trim();
+
+        final validatedContact = PresentationContact(
           matrixId: widget.externalContact.matrixId,
-          displayName:
-              profile.displayname ?? widget.externalContact.displayName,
+          displayName:
+              resolvedDisplayName?.isNotEmpty == true
+                  ? resolvedDisplayName
+                  : (fallbackDisplayName?.isNotEmpty == true
+                      ? fallbackDisplayName
+                      : widget.externalContact.matrixId),
           type: widget.externalContact.type,
         );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/contacts_tab/contacts_tab_body_view.dart` around lines 345 - 346,
The displayName assignment currently uses profile.displayname even when it's
blank/whitespace; update the resolution logic at the displayName field (where
profile.displayname and widget.externalContact.displayName are used) to treat
blank/whitespace as absent — e.g., check profile.displayname?.trim().isNotEmpty
and only use profile.displayname when that is true, otherwise fall back to
widget.externalContact.displayName; adjust the expression in the same assignment
so empty or whitespace server names do not override the fallback.

296-316: ⚠️ Potential issue | 🟠 Major

Profile future invalidation should also track widget.controller.client.

Current logic only keys on matrixId, so account/client swaps can reuse a future from a previous client.

Suggested fix
 class _SilverExternalContactState extends State<_SilverExternalContact> {
   Future<CachedProfileInformation>? _profileFuture;
   String? _currentMatrixId;
+  Client? _currentClient;
@@
   void didUpdateWidget(covariant _SilverExternalContact oldWidget) {
     super.didUpdateWidget(oldWidget);
-    if (oldWidget.externalContact.matrixId != widget.externalContact.matrixId) {
-      _fetchProfileIfNeeded();
-    }
+    _fetchProfileIfNeeded();
   }

   void _fetchProfileIfNeeded() {
     final matrixId = widget.externalContact.matrixId;
+    final client = widget.controller.client;
     if (matrixId == null) {
       _currentMatrixId = null;
+      _currentClient = null;
       _profileFuture = null;
       return;
     }
-    if (_currentMatrixId != matrixId) {
+    if (_currentMatrixId != matrixId || !identical(_currentClient, client)) {
       _currentMatrixId = matrixId;
-      _profileFuture = widget.controller.client.getUserProfile(
+      _currentClient = client;
+      _profileFuture = client.getUserProfile(
         matrixId,
         maxCacheAge: Duration.zero,
       );
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/contacts_tab/contacts_tab_body_view.dart` around lines 296 - 316,
The profile future invalidation only compares matrixId, so when the
account/client changes we may reuse a stale future; update the logic in
didUpdateWidget and/or _fetchProfileIfNeeded to also detect changes to
widget.controller.client (or its identity) and invalidate/reset
_currentMatrixId/_profileFuture when the client reference changes; specifically,
compare a stored client reference (e.g., _currentClient) against
widget.controller.client and if different set _currentMatrixId = null and
_profileFuture = null before calling widget.controller.client.getUserProfile to
ensure _profileFuture always corresponds to the current client.
lib/pages/search/search_external_contact.dart (1)

49-57: ⚠️ Potential issue | 🟠 Major

Refetch key should include active Matrix client, not keyword only.

The future cache can go stale when client identity changes and widget.keyword stays unchanged, because invalidation currently keys only on _currentKeyword.

Suggested fix
 class _SearchExternalContactWidgetState
     extends State<SearchExternalContactWidget> {
   Future<CachedProfileInformation>? _profileFuture;
   String? _currentKeyword;
+  String? _currentClientUserId;
@@
   void _fetchProfileIfNeeded() {
-    if (_currentKeyword != widget.keyword) {
-      _currentKeyword = widget.keyword;
-      final client = widget.clientForTesting ?? Matrix.of(context).client;
+    final client = widget.clientForTesting ?? Matrix.of(context).client;
+    final clientUserId = client.userID;
+    if (_currentKeyword != widget.keyword ||
+        _currentClientUserId != clientUserId) {
+      _currentKeyword = widget.keyword;
+      _currentClientUserId = clientUserId;
       _profileFuture = client.getUserProfile(
         widget.keyword,
         maxCacheAge: Duration.zero,
       );
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/pages/search/search_external_contact.dart` around lines 49 - 57, The
refetch logic in _fetchProfileIfNeeded currently only compares _currentKeyword
to widget.keyword, so changes in the active Matrix client
(widget.clientForTesting or Matrix.of(context).client) won't trigger a refresh;
add a stored client identity (e.g. a private field _currentClient) and include
it in the invalidation check: if either _currentKeyword != widget.keyword OR
_currentClient != client then update _currentKeyword and _currentClient and set
_profileFuture by calling client.getUserProfile(...). Use the same client
variable (widget.clientForTesting ?? Matrix.of(context).client) for comparison
and assignment so client swaps correctly trigger refetches.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/pages/search/search_external_contact.dart`:
- Line 79: The displayName assignment currently uses the null-coalescing
operator which treats empty or whitespace-only profile.displayname as present;
update the expression used where displayName is set (the displayName:
profile.displayname ?? widget.keyword.substring(1) line) to treat
empty/whitespace as missing by checking trimmed emptiness first — e.g. compute a
safeName = (profile.displayname ?? '').trim(); then use displayName:
safeName.isEmpty ? widget.keyword.substring(1) : profile.displayname (or
safeName) so blank names fall back to the keyword.

---

Duplicate comments:
In `@lib/pages/contacts_tab/contacts_tab_body_view.dart`:
- Around line 345-346: The displayName assignment currently uses
profile.displayname even when it's blank/whitespace; update the resolution logic
at the displayName field (where profile.displayname and
widget.externalContact.displayName are used) to treat blank/whitespace as absent
— e.g., check profile.displayname?.trim().isNotEmpty and only use
profile.displayname when that is true, otherwise fall back to
widget.externalContact.displayName; adjust the expression in the same assignment
so empty or whitespace server names do not override the fallback.
- Around line 296-316: The profile future invalidation only compares matrixId,
so when the account/client changes we may reuse a stale future; update the logic
in didUpdateWidget and/or _fetchProfileIfNeeded to also detect changes to
widget.controller.client (or its identity) and invalidate/reset
_currentMatrixId/_profileFuture when the client reference changes; specifically,
compare a stored client reference (e.g., _currentClient) against
widget.controller.client and if different set _currentMatrixId = null and
_profileFuture = null before calling widget.controller.client.getUserProfile to
ensure _profileFuture always corresponds to the current client.

In `@lib/pages/search/search_external_contact.dart`:
- Around line 49-57: The refetch logic in _fetchProfileIfNeeded currently only
compares _currentKeyword to widget.keyword, so changes in the active Matrix
client (widget.clientForTesting or Matrix.of(context).client) won't trigger a
refresh; add a stored client identity (e.g. a private field _currentClient) and
include it in the invalidation check: if either _currentKeyword !=
widget.keyword OR _currentClient != client then update _currentKeyword and
_currentClient and set _profileFuture by calling client.getUserProfile(...). Use
the same client variable (widget.clientForTesting ?? Matrix.of(context).client)
for comparison and assignment so client swaps correctly trigger refetches.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4a168800-ce2a-4162-92ed-e07a22112b44

📥 Commits

Reviewing files that changed from the base of the PR and between 92e6932 and 6e4e557.

📒 Files selected for processing (4)
  • integration_test/tests/chat/search_external_mxid_test.dart
  • lib/pages/contacts_tab/contacts_tab_body_view.dart
  • lib/pages/search/search_external_contact.dart
  • test/pages/search/search_external_contact_test.dart
✅ Files skipped from review due to trivial changes (1)
  • test/pages/search/search_external_contact_test.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • integration_test/tests/chat/search_external_mxid_test.dart

Copy link
Copy Markdown
Collaborator

@tddang-linagora tddang-linagora left a comment

Choose a reason for hiding this comment

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

  • Please share the integration test's video or result log.

@9clg6 9clg6 self-assigned this Mar 25, 2026
@9clg6
Copy link
Copy Markdown
Collaborator Author

9clg6 commented Mar 25, 2026

  • Please share the integration test's video or result log.

@hoangdat hoangdat merged commit 2db5910 into main Mar 27, 2026
9 checks passed
@hoangdat hoangdat deleted the feature/TW-2474-improve-searching-non-exist-matrix-address branch March 27, 2026 07:19
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