From 3b7b4a1ccb57a0c6f5f0d62e8ef3eae4ba472b9a Mon Sep 17 00:00:00 2001 From: Tom Brow Date: Tue, 10 Nov 2026 11:12:36 -0800 Subject: [PATCH] perf(mobile): watch Solo agent names only in Solo chats The chat header hook runs on every channel page, and it watched every identity name source even outside Solo, so any profile load rebuilt the whole page. Watch names only when the hook is enabled, and bind the guard to the existing channel isolation journey: an unrelated profile load must not rebuild the open channel page. Co-authored-by: corey <2dafccf60ea6d431a568f6d67fc600160d92a51e415e09dc8b213b8e360663d5@localhost> Co-authored-by: studio Co-Authored-By: Claude Opus 5.5 Signed-off-by: Tom Brow --- .../channels/me/me_chat_assistant.dart | 20 +++++++++---------- .../channels/channel_detail_page_test.dart | 18 +++++++++++++++++ 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/mobile/lib/features/channels/me/me_chat_assistant.dart b/mobile/lib/features/channels/me/me_chat_assistant.dart index 53683a36abe..c21c361065f 100644 --- a/mobile/lib/features/channels/me/me_chat_assistant.dart +++ b/mobile/lib/features/channels/me/me_chat_assistant.dart @@ -106,25 +106,25 @@ useMeChatAssistant( } } // The owner's inventory name outranks the agent's self-published profile. - final names = watchMeAssistantNames( - ref, - agents, - saved: [selected, pending?.agent], - ); + // Only Solo reads these names; other chats must not watch every profile. + final names = enabled + ? watchMeAssistantNames(ref, agents, saved: [selected, pending?.agent]) + : null; // A known agent outside the inventory, such as an archived one, is labeled // against the live agents so it never reads as a live namesake. bool named(String pubkey) => - agents.any((agent) => agent.pubkey == pubkey) || - ref.read(userCacheProvider)[pubkey]?.displayName?.trim().isNotEmpty == - true; + names != null && + (agents.any((agent) => agent.pubkey == pubkey) || + ref.read(userCacheProvider)[pubkey]?.displayName?.trim().isNotEmpty == + true); final name = selected != null && named(selected) - ? names.labelFor(selected) + ? names!.labelFor(selected) : profile?.displayName; final avatarInitial = name?.trim().isNotEmpty == true ? name!.trim()[0].toUpperCase() : profile?.initial ?? '?'; String agentName(String pubkey) { - if (named(pubkey)) return names.labelFor(pubkey); + if (named(pubkey)) return names!.labelFor(pubkey); return pubkey.length > 12 ? '${pubkey.substring(0, 8)}…${pubkey.substring(pubkey.length - 4)}' : pubkey; diff --git a/mobile/test/features/channels/channel_detail_page_test.dart b/mobile/test/features/channels/channel_detail_page_test.dart index 7757af969f0..a9aef130cab 100644 --- a/mobile/test/features/channels/channel_detail_page_test.dart +++ b/mobile/test/features/channels/channel_detail_page_test.dart @@ -5033,8 +5033,10 @@ void main() { _testChannel, otherChannel, ]); + final profiles = _FakeUserCacheNotifier({}); await tester.pumpWidget( _buildTestable( + userCacheNotifier: profiles, messages: [ for (var i = 0; i < 5; i++) _textMsg( @@ -5062,6 +5064,22 @@ void main() { await tester.pumpAndSettle(); expect(rebuilt.whereType(), isEmpty); + + // A profile learned in another surface must not rebuild this ordinary + // channel: the Solo naming hook is disabled here. + rebuilt.clear(); + profiles.replace( + UserProfile(pubkey: 'f' * 64, displayName: 'Unrelated profile'), + ); + await tester.pumpAndSettle(); + expect( + rebuilt.where( + (widget) => + widget.runtimeType.toString() == '_ResolvedChannelDetailPage', + ), + isEmpty, + reason: 'an unrelated profile must not rebuild the open channel page', + ); }); testWidgets('clears the composer inset when membership is revoked', (