Repository navigation
mobile: persist user profiles so names and avatars render on launch - #9116
Conversation
…tars Restore verified kind:0 profiles from a per-identity, per-relay snapshot on launch so names and avatar URLs render before the relay answers, then revalidate each restored profile once per session without blocking callers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tom Brow <tomb@squareup.com>
A revalidation in flight no longer marks its restored profile as confirmed, so a concurrent resolve() still queries the relay. Repeat lookups skip pubkeys already on the wire. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tom Brow <tomb@squareup.com>
|
@codex review |
Codex Review Summary: ✅ CompletedThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 94e82dd64c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!_isCurrent(generation)) { | ||
| throw StateError('Profile request belongs to a retired cache'); | ||
| } | ||
| _unrevalidated.removeAll(toFetch); |
There was a problem hiding this comment.
Evict restored profiles absent from revalidation
When a profile restored from disk has since been deleted from the relay, a successful empty history response reaches this line and merely marks the old entry as revalidated; state and _profileEventOrders still retain and later re-persist its stale name and ownerPubkey. Consequently, resolve()/refresh() report success while identity-sensitive DM logic can continue treating a deleted agent profile as authoritative. Remove requested restored entries not represented by a successful batch response before clearing _unrevalidated.
AGENTS.md reference: AGENTS.md:L229-L235
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
[P2] Evict absent restored profiles before marking them confirmed
There’s a concrete downstream effect here: the DM view refreshes participant profiles, then uses cached ownerPubkey to classify agent DMs and hide Huddle. An empty successful response leaves the restored owner hint in place while marking it confirmed, so that classification can remain stale. Could successful revalidation remove absent restored entries and their ordering metadata, and persist that removal?
Posted by Codex (gpt-6.1-sol) on behalf of kylez
| .then( | ||
| (saved) { | ||
| if (!saved) debugPrint('[UserCache] snapshot was not saved'); | ||
| }, | ||
| onError: (Object error, StackTrace stack) { | ||
| debugPrint('[UserCache] snapshot write failed: $error'); |
There was a problem hiding this comment.
Retain failed snapshot writes for retry
When SharedPreferences.setString returns false or throws, this continuation only logs the failure after the debounce timer has already been cleared. If no later profile mutation happens, the cache remains dirty only in memory and the next cold launch loses every profile learned in that session; preserve a retry record and retry with bounded backoff or propagate the failure instead of abandoning the write.
AGENTS.md reference: AGENTS.md:L220-L227
Useful? React with 👍 / 👎.
| ); | ||
| _unrevalidated.add(key); | ||
| } | ||
| return _latest = restored; |
There was a problem hiding this comment.
🤖 P2 · Restored picker profiles never start verified-name discovery
Restoring entries into state makes watchIdentityNames’ presence check skip preload for those displayed identities. These selector-only consumers therefore never reach _scheduleFetch or _trackVerified; _unrevalidated alone does not initiate either operation. Agents and recipient pickers are affected even with a healthy relay.
User/system effect: After a cold launch, Agents can keep showing an old cached name/avatar over fresh directory information. A snapshotted recipient with a different available NIP-FI verified name keeps their self-declared name in search results and selected Flutter chips/native rows until an unrelated lookup registers them. Uncached recipients do start that discovery.
Test: Persist a recipient’s kind:0 profile, restart, then search for and select them without another profile lookup. Return a different verified name from the relay and check that it is requested and replaces the cached label while cached content renders immediately.
🔐 Codex Security Review
Review SummaryOverall Risk: NONE
FindingsNo concrete security, correctness, or reliability findings were identified. Notes
Generated by Codex Security Review | |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 94e82dd64c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ); | ||
| _unrevalidated.add(key); | ||
| } | ||
| return _latest = restored; |
There was a problem hiding this comment.
Trigger revalidation for direct cache consumers
When the app cold-launches directly into a surface such as Agents, watchIdentityNames treats every restored key as present and only preloads missing keys (identity_names_provider.dart:67-75), so it never calls get or preload for these entries. Returning the restored map here without scheduling its entries therefore lets an old name, avatar, or owner attribution remain in use for the entire session unless an unrelated surface later requests that pubkey; direct provider-state consumers need to schedule first-use revalidation too.
AGENTS.md reference: AGENTS.md:L229-L235
Useful? React with 👍 / 👎.
kazemek21
left a comment
There was a problem hiding this comment.
Kyle reviewed the PR and the Nexus review-tracker notes and approved with the non-blocking comment above.
Posted by Codex (gpt-6.1-sol) on behalf of kylez
tlongwell-block
left a comment
There was a problem hiding this comment.
🤖 Meli, Tyler’s AI agent, posting on his explicit behalf.
Approved as directed by Tyler.
Full mobile suite passed (4549 main, 67 skipped, 4 push, 27 crypto). Repeated cold-offline Home captures corroborate persisted display-name restoration. They do not establish sub-five-second restoration, timeline hydration or avatar bitmap persistence. An earlier intermittent test failure did not recur in 40 alternating main/PR runs; its cause remains unattributed.
Reviewed head: bdbd4a48c130a12a6a8816dde3adba13194fcb9d.
…ction-p * origin/main: (111 commits) feat(mobile): tag the reacted-to message author on reactions (#9141) fix(mobile): steady New DM picker rows and animate picks (#9249) perf(mobile): keep remote images on disk and show small ones as thumbnails (#9158) fix(relay): deliver slow cross-node events late instead of closing their recipients (#9235) fix(mobile): read only shown events from filtered Activity rows (#9215) fix(mobile): show the channel name as the notification subtitle (#9173) fix(mobile): keep Home's header clean under sheets (#9152) fix(mobile): show already-loaded custom emoji in the iOS reactions sheet (#9150) mobile: add follow and remind actions to inbox row menu (#9142) fix(mobile): start no threads in Solo chats (#9123) mobile: persist user profiles so names and avatars render on launch (#9116) fix(mobile): keep a sent thread reply above the composer (#9026) [buzz] Recover database pool capacity after stalled connections (#9267) feat: send a User-Agent on relay WebSocket upgrades (#9246) Record why relay connections close and quiet close warnings (#9245) fix(relay): say "channel owner or admin" in the call_webhook save refusal (#9169) feat(mobile): consolidate Solo chat settings in one sheet (#9255) fix(cli): preserve attachments across successive edits (#9226) fix(mobile): swipe left to reply so swipe-back keeps rightward drags (#9127) mobile: long-press a message link for open, copy and share (#9135) ... Signed-off-by: Matthew Boston <mboston@squareup.com>
…-profile * origin/main: (307 commits) fix: inline ARM64 atomics in Sprig image build (#9291) fix(mobile): show observed thread replies in Activity (#9286) Enable mobile huddles with Codex agents (#9269) fix(mobile): read mentions taller than the screen (#9243) test(relay): give every media e2e upload its own blob hash (#9272) Add invite preview video and fallback poster (#9282) NEXUS-4336: Wait for desktop-provider socket ownership proof (#9270) fix: unblock image audit with patched MCP SDK (#9287) feat(ci): allow admin dispatch of desktop candidate validation (#9288) feat(mobile): tag the reacted-to message author on reactions (#9141) fix(mobile): steady New DM picker rows and animate picks (#9249) perf(mobile): keep remote images on disk and show small ones as thumbnails (#9158) fix(relay): deliver slow cross-node events late instead of closing their recipients (#9235) fix(mobile): read only shown events from filtered Activity rows (#9215) fix(mobile): show the channel name as the notification subtitle (#9173) fix(mobile): keep Home's header clean under sheets (#9152) fix(mobile): show already-loaded custom emoji in the iOS reactions sheet (#9150) mobile: add follow and remind actions to inbox row menu (#9142) fix(mobile): start no threads in Solo chats (#9123) mobile: persist user profiles so names and avatars render on launch (#9116) ... Signed-off-by: Franco Sola <fsola@squareup.com> # Conflicts: # docs/nips/NIP-FA.md # docs/nips/NIP-FI.md
On a slow connection, mobile shows npubs and blank avatars on every cold launch and community switch, because user profiles live only in memory and are refetched from the relay each time.
This persists the verified profiles the app has already seen, per identity and relay, so names and avatar URLs render on launch before the relay answers.
Behavior
getandpreloadreturn the restored data right away instead of waiting on the network, and a failed revalidation retries on the next lookup.resolve()andrefresh()treat restored profiles as unconfirmed and query the relay, including while a background revalidation is still in flight.Not in this PR: avatar image bytes still use only Flutter's in-memory image cache, so avatar images still download after a cold launch (names and URLs no longer wait). The NSE keeps its own app-group profile cache, which this doesn't share yet.
Demo
Cold relaunch into a synthetic Solo chat ("QA demo"), iPhone 17 Pro simulator, same clip timing on both builds. To make the gap visible on a fast connection, both builds had a local-only 5 s delay on profile fetches (not part of this PR). The channel's system lines show the member name once its profile is known and a shortened npub until then.
Before (main): "npub1...created this channel" for about 5 s until the relay answers, and the agent avatar starts as a letter placeholder.
before.mp4
After (this branch): "tomb created this channel" and the agent avatar render from the first frame.
after.mp4
Validation
resolve()during revalidation, and sign-out removal; plus storage scoping, bounds, writer retirement and corrupt-data handling. The revalidation and in-flight guards were each removed once to confirm the journey fails.🤖 Generated with Claude Code