Skip to content

fix: speed up pubky profile loading - #1399

Merged
Jasonvdb merged 74 commits into
masterfrom
claude/flow-vibe-pubky-profile-load-lag
Oct 3, 2026
Merged

Jasonvdb merged 74 commits into
masterfrom
claude/flow-vibe-pubky-profile-load-lag

Conversation

@Jasonvdb

@Jasonvdb Jasonvdb commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Twin: synonymdev/bitkit-ios#854

Fixes the lag when loading pubky profiles and importing contacts, which got much worse with the shared Pubky Ring identities from #1329.

Where the lag came from. All Paykit calls go through one lock, and each call kept it for its whole network round trip. That included simple public reads, like fetching a profile name or avatar, or checking how a contact can be paid. So lookups queued behind each other, behind sign-in, and behind background Paykit work. A pubky that was never published takes 2–7 s to fail, and everything else waited for it. On top of that:

  • The Pubky Ring choice screen looked up every Ring identity one at a time and hid the list until all of them were done.
  • Importing contacts looked up every follow a second time, with a retry, even though the import screen had just found them. Any follow whose lookup failed was silently dropped.
  • The Contacts screen waited for every contact's profile before showing anything, and those lookups queued behind the import's. Your own avatar queued behind them too.

How this fixes it. Public reads skip the lock and run side by side, up to six at once. Background reads (the contact list, private payment setup) may use at most four of those six, so what you are looking at (your avatar, your profile, the Ring rows) never waits behind them. Anything that touches your session, keys or saved Paykit data still goes through the lock. The choice screen shows its rows straight away with a spinner on each. The import keeps running if you leave the screen. Saving the imported contacts without new lookups came from #1395, which this branch now includes from master. Contacts shows your saved contacts at once and fills in names and avatars as they load.

Emulator, staging network, one take per build Before After
Ring rows visible 10.8 s 0.2 s
Published name visible 10.8 s 2.5 s
Tap a Ring row → Pay Contacts (identity with no follows) 11.0 s 5.4 s
Tap a Ring row → import screen (8 follows, 6 never published) 63.6 s 10.9 s
Import All → Pay Contacts 44.0 s 0.9 s
Follows saved as contacts 2 of 8 8 of 8
Profile avatar right after the import spins for 7.4 s shows at once

The Import All and saved-follows rows were measured against master before #1395, which now gives master the same import save.

With QA's 61-follow Pubky Ring profile (emulator, staging network; 54 follows have profiles and 7 were never published):

master This PR before the Contacts fixes This PR now
Follows found → import screen 176 s 20–67 s 26–31 s
Import All → contacts saved 41 s 28–41 s 13–19 s
Contacts: profile lookups per visit – 61 7 (only contacts with no profile)
Contacts: list updates after opening – 113 1
Contacts: janky frames, open / scroll 16% / 26% 44–48% / 47–55% 13–21% / 15–28%

"Let your contacts pay you" still takes about 2 minutes 15 seconds (5 minutes 39 seconds on master), and Delete Profile with 61 contacts can take minutes. See Out of Scope.

Description

  • Public profile, avatar, follows and payment-route reads no longer wait for the Paykit lock. Up to six run at once, at most four of them for background work, a freed slot goes to what you are looking at before queued background reads, and they stop when their screen closes
  • Sign-in starts the identity refresh in the background instead of waiting up to 5 s for it; auth approvals still wait for it, including a refresh that is already running
  • The Pubky Ring choice screen shows its rows at once, with a spinner on each row while its profile loads and on the row being adopted
  • Adopting a Ring identity reuses the profile its row already loaded
  • A contact import keeps running if you leave its screen; Select and Import All are disabled while it runs. Each imported contact is saved only if the Paykit identity is still the one the import started for, checked under the Paykit lock. Leaving while it runs keeps the follows it has not saved yet, a failure after you left still shows an error, and Pay Contacts opens only after a successful import
  • Contacts shows saved contacts at once with their saved names, then fills in profiles as they load. Opening a contact whose profile hasn't loaded yet looks it up at once, ahead of the background refresh, and editing or tagging it waits for that one lookup, so an edit can't save over a profile that is still arriving. If the lookup fails, the edit saves as it does on master
  • Profile lookups retry only where it helps (adding a contact, loading your own profile), and never after "not found"
  • The Profile screen shows your cached name and avatar while it loads, and loads once more on its own if a load that was already running fails
  • Pubky avatars are cached on disk and cleared on sign-out or when the identity changes. An avatar that failed because it is missing or too large is not fetched again until that cache is cleared. Other failures are tried again after 60 s
  • Each follow lookup on the import screen stops after 10 s of Paykit work, and the follow then shows under its key. Time spent waiting for a read slot does not count toward the 10 s
  • Contacts does not look up again a profile it found in the last 10 minutes, and applies refreshed profiles at most once every 300 ms, so rows don't jump while names arrive
  • Refreshing private payment links no longer fails when a contact is saved during the refresh
  • A profile load that finishes after a newer save, adopt or sign-out is ignored, so it cannot overwrite newer data
  • Public reads are refused during a wallet wipe, and a read the wipe overtakes is dropped
  • An auth link that opens Bitkit after the system closed it (for example Enable Payments in pubky.app) now waits for your session to be restored instead of showing "Pubky Identity Required". If a saved profile can't be restored, it shows "Couldn't Load Your Pubky Profile" with a hint to try again

#1395. #1395 merged, and this branch now includes it from master; its import save replaced this branch's own. What this PR adds to the import: it keeps running after you leave, checks the identity under the Paykit lock before each save, and fills the session profile cache so Contacts shows the new contacts at once.

Out of Scope

  • app/src/main/java/to/bitkit/repositories/PubkyRepo.kt: app launch still builds the Paykit SDK and restores the session twice; removing that needs Paykit input
  • Adopting a never-published Ring pubky fails because Paykit's identity check throws instead of returning false; this needs a Paykit fix
  • paykit-rs: a never-published pubky still takes 2–7 s to fail its lookup, because pkarr NotFound surfaces as a transport error. That is most of the remaining 10.9 s before the import screen
  • Private payment link setup after an import still retries often and holds the Paykit lock for each attempt; changing its timing needs Paykit input
  • Contacts are still saved one at a time, because Paykit has no batch save
  • Editing a contact whose profile lookup failed still saves an empty bio, avatar and links over the published ones, as on master; saving only the changed fields needs a new override format
  • Contacts still looks up, on each visit, every contact without a profile (for example a never-published follow). Each lookup takes several seconds to fail
  • "Let your contacts pay you" and Delete Profile are slow with many contacts. Both wait on the private payment link setup walking every contact one at a time, and Delete waits for a running walk to finish. feat: share paykit state across apps #1401 rewrites that code
  • rustls_platform_verifier "BKS KeyStore not available" TLS errors on the emulator image; they were there before this PR and can make a lookup fail

How to review

Production code is about a quarter of this PR; most of the rest is tests (about 2,590 lines, with near-duplicate cases merged into case tables). docs/pubky.md is the shortest summary of the new rules. Suggested order, one area at a time:

  1. Paykit read path (the core fix): PaykitSdkService.kt (publicRead, PaykitReadLane, the background identity refresh), PaykitSdkOperationLock.kt (the wipe check without the lock) and PubkyService.kt (cancellable reads). Tests: PaykitSdkServiceTest, PaykitSdkOperationLockTest, PaykitSdkServiceWipeTest, PubkyServiceTest, PubkyIdentityRepublishTest
  2. Profile and Ring choice screen: in PubkyRepo.kt, loadProfile, fetchDisplayProfile, adoptRingIdentity and the profile writes; then PubkyChoiceViewModel/Screen, ProfileViewModel/Screen, PubkyImageFetcher.kt, PubkyImageCacheEpoch.kt and PubkyStore.kt. Tests: PubkyRepoTest, PubkyChoiceViewModelTest, ProfileViewModelTest, PubkyImageFetcherTest, PubkyStoreTest
  3. Contacts: in PubkyRepo.kt, loadContacts, ContactProfileRefresh (with its batches and freshness window), resolvePendingContactProfile, importContacts and prepareImport (the per-follow deadline); then the contacts view models and screens. Tests: PubkyRepoTest, ContactsViewModelTest, ContactDetailViewModelTest, EditContactViewModelTest, ContactImportOverviewViewModelTest, ContactImportSelectViewModelTest
  4. Payment checks and private sync: PrivatePaykitRepo.kt and PaykitPaymentRequestRepo.kt, which only choose a read lane
  5. Auth link wait: AppViewModel.kt (awaitPubkyDeeplinkInitialization), PubkyRepo.awaitIdentityReady and strings.xml. Test: AppViewModelSendFlowTest
  6. Journeys: journeys/pubky-profile/

What to look for: reads no longer queue behind the Paykit lock, so a result can now arrive late, after a sign-out, an identity switch, a newer save or a wallet wipe. Most of the new code drops those late results (owner checks, generation counters, the wipe check). Each of those guards has a test that holds a fake Paykit call halfway, runs the competing action, then releases the call.

Design

N/A — no design available. The spinners reuse existing components.

Preview

Choosing a Pubky Ring identity

android-before-after.mp4

Left: master (dbbf90f). Right: this PR (1f4364f; later commits do not change this flow). Same emulator, same taps, fresh app data, staging network, one take each. After the profile intro, master shows only "Loading your profile…" for about 11 s. With this PR the rows show at once, each row's spinner turns into its name or avatar, and adopting spins only the tapped row.

Setting up a profile and importing contacts

android-import-before-after.mp4

Left: master (5e990b1). Right: this PR (494db5f, recorded before #1395 merged; the import save now comes from #1395). Same emulator, same taps, staging network, one take each, aligned on the Ring row tap. Each take chooses the Pubky Ring identity, taps Import All, then opens Contacts and Profile. The test identity has no real pubky.app follows, so both builds use the same debug-only hook that returns 8 follows: 2 published and 6 never published, all created for this test. On master the import screen takes about 64 s, Import All about 44 s, only the 2 published follows are saved, and the Profile avatar spins for about 7 s. With this PR the import screen takes about 11 s, Import All about 1 s, all 8 follows are saved, and the avatar shows at once.

QA Notes

Journeys

  • new cached-profile-header.xml — Profile opened while loading shows the cached name and avatar without edit actions, then the full profile
  • new ring-choice-rows.xml — Pubky Ring rows show at once with a spinner each until their profile loads; adopting spins only the tapped row and disables the rest
  • new contact-import-after-leaving.xml — leaving the import screen right after Import All stays on Home, never opens Pay Contacts, and Contacts later lists every follow
  • new contacts-list-loading.xml — saved contacts show at once with their stored names, profiles fill in, and the full-screen spinner shows only until the first load

Manual Tests

  • With Pubky Ring holding a published pubky and a never-published one, open the profile choice screen → both rows show at once with spinners, and the published name appears without waiting for the other row — Pubky Ring not in Capabilities
  • Tap the published Ring row → only that row spins, the other row is disabled, and Pay Contacts opens — Pubky Ring not in Capabilities
  • Force an adopt failure (airplane mode right after the tap) → the rows stay visible with their names and can be tapped again — Pubky Ring not in Capabilities
  • Adopt a Ring identity that follows several pubkys, some never published, then Import All → every follow is saved (unpublished ones under their key), Contacts lists them at once, and the Profile avatar shows — Pubky Ring not in Capabilities
  • With a saved Pubky identity: turn on airplane mode, force-stop Bitkit, open a pubkyauth:// link (Enable Payments in pubky.app), and turn airplane mode off within a few seconds → the approval sheet appears, not "Pubky Identity Required"; if you stay offline → "Couldn't Load Your Pubky Profile" — airplane mode is outside journey capabilities

Automated Checks

  • added PubkyStoreTest.kt — the cached profile owner
  • added ContactsViewModelTest.kt — the full-screen spinner shows only until the saved contacts first load
  • updated PaykitSdkServiceTest.kt — a read deadline counts only the time a read holds its slot, a contact save queued behind an identity change is not saved to the new identity, public reads and locked work do not wait for each other, reads are capped at six with two kept for what you are looking at, a freed read slot goes to an interactive read before queued bulk reads, a cancelled slot waiter takes no slot and passes on one it was handed, receiver reads run outside the lock, a read with no SDK builds it under the lock, activation does not wait for the refresh, and an approval waits for a refresh activation started
  • updated PaykitSdkOperationLockTest.kt and PaykitSdkServiceWipeTest.kt — public reads are refused during a wallet wipe and dropped when a wipe overtakes them
  • updated PubkyServiceTest.kt — cancelling a public read cancels the underlying call
  • updated PubkyIdentityRepublishTest.kt — approvals still wait for the identity refresh
  • updated PubkyRepoTest.kt — stale profile loads are dropped after a write, the retry policy per caller, the adopt handoff, the import outliving its screen, stopping on an identity change and clearing the pending import only on success, the Contacts list publishing before lookups finish, skipping lookups for profiles found in the last 10 minutes, applying refresh results in batches and dropping a stale batch, the per-follow deadline on the import screen, and the auth link waiting for a session restore
  • updated PrivatePaykitRepoTest.kt, PaykitPaymentRequestRepoTest.kt and PaykitPaymentRequestRepoSubscriptionTest.kt — background and interactive read slots for private sync and payment checks, and a private link refresh that survives a contact being saved mid-refresh
  • updated ContactImportOverviewViewModelTest.kt, ContactImportSelectViewModelTest.kt, ContactDetailViewModelTest.kt and EditContactViewModelTest.kt — repeat taps ignored while an import runs, the overview leaving once its import finishes elsewhere, and loading a contact's profile before editing it
  • updated AppViewModelSendFlowTest.kt — a contact import failure shows an error after the import screens are gone, the auth link waits for a session retry, shows the retryable error for a saved identity it can't restore, and private sync ignores contact row updates
  • updated PubkyChoiceViewModelTest.kt — rows show before lookups finish, each row's spinner clears whether its lookup finds a profile, finds nothing or fails, and lookups continue after a failed adoption
  • updated ProfileViewModelTest.kt — cached header only while loading and only for the current key, refresh on open, and one more load when a running load fails
  • updated PubkyImageFetcherTest.kt — disk cache hits, no writes after a clear or for a failed download, disk errors falling back to the network, cancellation passing through the disk cache, and failed fetches remembered until a cache clear (missing or too large) or for 60 s (other errors)

@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

[Medium risk] Adds caching and concurrency controls to profile loading.

The PR should not merge until an overlapping profile refresh can no longer restore stale cached metadata after a save or sign-out.

Findings

  1. P1 Stale refresh overwrites cached metadata ▶
  2. P2 Cleared avatars can return ▶

Summary

The PR moves public Pubky reads out of the Paykit operation lock, loads Ring identity rows independently, reuses resolved profiles during adoption, and adds cached profile and avatar presentation.

  • Profile refreshes need to keep their persisted metadata from overtaking newer writes or sign-out.
  • Avatar cache clearing has a narrow commit race with in-flight fetches.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Profile or Ring screen] --> B[PubkyRepo]
  B --> C[PubkyService]
  C --> D[PaykitSdkService public-read permits]
  D --> E[Public Pubky data]
  B --> F[Profile state and metadata cache]
  E --> G[PubkyImageFetcher]
  G --> H[Avatar disk cache]
Loading

Reviews (1) · Last reviewed commit: "fix: refresh the profile on open and tol..."

Comment thread app/src/main/java/to/bitkit/repositories/PubkyRepo.kt Outdated
Comment thread app/src/main/java/to/bitkit/data/PubkyImageFetcher.kt
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Regtest APK

Built from b46f007 (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

A Ring row now shows a spinner in its avatar slot until its profile
lookup finishes, whatever the outcome, so a row still looking up no
longer looks like one whose identity has no profile. The adoption
spinner moves into the row's key icon circle, matching iOS.

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Scope: Follow-up of the changes since b001a3c, the head of the previous full review, at 97bde5d. The comparison base and merge base are still 6ba44a4079c5ee02625eac59a56d6a55a6297431. This pass covered the contact-import leave, failure, and identity-change delta. Inherited baseline: the full pull request diff reviewed at b001a3c, including public profile reads, stale profile writes, and the import owner check under the Paykit lock.

No new actionable code findings.

Leaving the import overview no longer drops a failed background save. Back and the drawer call discardPendingImport(), which keeps the pending follows while a save is still running. Pay Contacts opens only after a successful import, and Back on the overview does not open it. A failed import sets contactImportFailure, and AppViewModel shows that error after the import screens are gone. An import stopped because the identity changed does not raise that toast.

Validation: build, lint, and detekt succeeded on this revision. The new unit tests in PubkyRepoTest.kt, ContactImportOverviewViewModelTest.kt, and AppViewModelSendFlowTest.kt were inspected and not executed here. Device testing was not performed. bitkit-ios#854 was not reviewed.

Device testing: not performed in this review.

Ready for device testing.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

Testing Bitkit <> Ring profile import. Staging.

logs.zip

Screen.Recording.2026-10-02.at.11.08.41.mov

Can be compared with logs from #1406 (comment)... the same profile was imported. improvement is mainly on step 2.

@piotr-iohk

Copy link
Copy Markdown
Collaborator

One nit that could be addressed here. After tap on "Delete profile" currently there's no spinner screen. It is present on iOS.

refreshKnownSavedContactEndpoints iterated the live saved contact key set
across suspending receiver lookups. A contact saved while a lookup was
suspended mutated the set and the resumed loop threw
ConcurrentModificationException.
@Jasonvdb

Jasonvdb commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. Timings from your logs, compared with the #1401 run on the same 61-follow profile:

Step, from log timestamps #1401 at 5fb0193 This PR at 97bde5d
Follows discovered to preview ready about 2 min 52 s about 5 min 3 s
Preview ready to Imported '61' contacts, including any wait before the tap at most 3 min 35 s at most 11 s
Continue not done after 15 min still running when the log ends, 3 min in

The logs don't show when Import was tapped, so the middle row is an upper bound. Even so, the import itself took at most 11 s on this build, which matches the gain you saw in step 2.

Preview. This took longer on this run, because of 3 follows whose profile lookup took about 5 minutes to fail with transport_error: pubky6f…bkei55y, pubkytf…4gji7wy and pubkyzz…ddt4bwo. They are among the same 7 follows from #1406. Two of them failed in the same millisecond, 09:13:33.287. That points to one shared stalled connection, probably a homeserver that cannot be reached, rather than to the app. This PR makes one lookup attempt per follow in the preview, where master makes two. On the #1401 run, though, the same follows failed within 12 s to 1 min 48 s per attempt, so the length of this step depends on how long the network layer takes to give up. Nothing in this PR limits how long one preview lookup can take.

Continue. This is still the receiver-path walk from #1406, which #1401 covers. At the end of this log it is still retrying the same 7 follows.

One more thing in the log. At 09:13:48, Failed to refresh private Paykit endpoints for 'payment request polling' failed with a ConcurrentModificationException. The polling refresh walked the live set of saved contact keys while the import was adding to it. The race is older than this PR, but the faster import makes it more likely. Fixed in 58d5c53, with the regression test refreshKnownSavedContactEndpoints succeeds when a contact is saved during receiver discovery in PrivatePaykitRepoTest.kt.

Next, I'm reproducing your run with about 60 follows to look at the preview time and any other lag.

Copy link
Copy Markdown
Collaborator

One nit that could be addressed here. After tap on "Delete profile" currently there's no spinner screen. It is present on iOS.

No need to address this here. #1394 adds deletion progress using a spinner on the Delete Profile button. I’ll raise the full-screen presentation difference there.

@Jasonvdb

Jasonvdb commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, I'll leave the delete spinner to #1394 and won't change it here.

@Jasonvdb

Jasonvdb commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my earlier reply: the 7 failing follows are not behind an unreachable homeserver. Six of them have no pkarr record on the relays, and the seventh (pubky6f…bkei55y) points to a homeserver key that has no record either, so none of them can resolve.

In our 61-follow runs on an emulator, using the same follow list, they fail in 5–9 s each, as on iOS. The 5-minute failures did not reproduce. The SDK limits each HTTP request to 30 s, but the pkarr and DHT lookups have no overall deadline. A slow DHT on your network is the most likely cause, but that is unconfirmed.

A follow whose profile read is still running 10 seconds after it got a read slot is cancelled, which frees the slot, and shows in the preview as a placeholder. Waiting for a slot does not count, and the timeout surfaces as PaykitReadTimeoutError, an ordinary failure.
A Contacts load no longer looks up again every contact whose profile this session already resolved, so returning from a contact's screen does not re-resolve the whole list. Contacts without a resolved profile are still looked up on every load, and sign-out or an identity change clears the state.
A background refresh now updates the contact list at most once every 300 ms, sorting it once per batch, and applies the last batch as soon as its lookups finish. A batch overtaken by a sign-out or an identity change is dropped, and a contact screen applies a profile still waiting for its batch at once.
A missing, oversize or invalid avatar is not fetched again until the image cache is cleared, and an avatar whose fetch failed for another reason is retried after 60 seconds. Sign-out and an identity change forget every failure.
@piotr-iohk

Copy link
Copy Markdown
Collaborator

Repeated the test on Samsung s22 device. The results are similar as on emulator. Actually step 2 here was also slow. Step 3 the slowest. I'm ~15 min in and still in progress. Will post logs once it finishes.

@piotr-iohk

piotr-iohk commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Device retest of the 61-follow Ring import on a Samsung, build 58d5c53. Timestamps are from bitkit_2026-10-02_13-31-21.log.

Step, from log timestamps #1401 at 5fb0193 This PR on the emulator at 97bde5d This PR on the Samsung at 58d5c53
Follows discovered to preview ready about 2 min 52 s about 5 min 3 s about 15 s
Preview ready to Imported '61' contacts, including any wait before the tap at most 3 min 35 s at most 11 s at most 2 min 32 s
Continue not done after 15 min still running when the log ends, 3 min in finished; last marker failure 4 min 54 s after import, then no further Paykit failures through 13:50

The seven unpublished follows failed in about 5–15 s. The recording is trimmed to the import list and arrival on Pay Contacts.

bk1399-import-samsung.mp4

bitkit_logs_2026-10-02_13-48-14.zip

@Jasonvdb

Jasonvdb commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the device run. I've pushed b46f007, which merges master (including #1394 and the Ring package rename from #1414) and adds four fixes for the lag in your 61-follow runs. I checked all four on an emulator with your follow list:

  • Import screen: each follow lookup now stops after 10 s of Paykit work, and that follow shows under its key. A stuck lookup, like the 5-minute ones in your first run, can no longer hold up the screen. Time spent waiting for a read slot doesn't count toward the 10 s.
  • Contacts lookups: Contacts no longer looks up a profile again if it found it in the last 10 minutes. With your 61 contacts, opening Contacts or coming back from a contact now makes 7 lookups (only the never-published follows) instead of 61.
  • Contacts updates: refreshed profiles are applied at most once every 300 ms. On opening Contacts, the list now updated once instead of 113 times, and rows no longer jump.
  • Avatars: an avatar that is missing or over 1 MB is fetched once instead of on every redraw.

Janky frames on Contacts dropped from 44–55% to 13–28% (16–26% on master).

Not changed here: "Let your contacts pay you" (about 2 min 15 s on the emulator) and Delete Profile with many contacts. Both wait on the private payment link setup, which walks every contact one at a time. #1401 rewrites that code.

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

QA review

Re-reviewed the full PR at b46f007c for an independent second pass, including affected callers, SDK contracts and tests.

No additional actionable code findings from the full reassessment.

The earlier background import failure and stale metadata concern are fixed in the current source. The remaining private-payment setup latency described in the latest performance update still needs runtime measurement; its serialized path predates this PR. This review does not establish that it is resolved.

Validation: 285 Android unit tests passed across nine relevant classes at this head, covering profile/import state, contact editing, image caching, SDK read admission and Ring choice state. Broader test coverage was inspected. Native throughput and cancellation latency were not profiled.

The Android/iOS shared behavior and all four profile journeys were independently reassessed. One additional queued-tag lifecycle finding belongs to iOS #854; it does not affect this Android implementation.

Suggested additional test cases

  • Android: with 60+ contacts and delayed profile responses, open a rich contact, add a tag and save. Edit its name while other profiles finish. Expect bio, avatar and links preserved, and unsaved fields kept intact.
  • Android: start a large import, leave the screen, then sign out or wipe and activate another identity before it finishes. Expect no old contacts, completion navigation or old-identity failure toast on the new identity.
  • Android: while contact refresh and private endpoint discovery run, open an auth link and a contact payment. Record elapsed waits separately for public reads and serialized private work; verify cancellation/recovery and absence of duplicate operations.
  • Android, with PIN enabled: let cold auth restoration time out, then scan an ordinary invoice. Expect the normal invoice confirmation to remain reachable after unlock.

Device testing: not performed in this review. Earlier device evidence remains attributed to its original builds.

@ovi-reviewer ovi-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Verdict: ✅ Approve

Review: diff 52 files.

QA:
Tested on Android 16 emulator (Pixel 10 Pro), staging.

🟢 Tests 1, 2, 3, 4, 5, J1, J2, J3, J4
Test 1

With Pubky Ring holding a published pubky and a never-published one, open the profile choice screen → both rows show at once with spinners, and the published name appears without….

1.mp4

Test 2

Tap the published Ring row → only that row spins, the other row is disabled, and Pay Contacts opens — Pubky Ring not in Capabilities.

2.mp4

Test 3

Force an adopt failure (airplane mode right after the tap) → the rows stay visible with their names and can be tapped again — Pubky Ring not in Capabilities.

3.mp4

Test 4

Adopt a Ring identity that follows several pubkys, some never published, then Import All → every follow is saved (unpublished ones under their key), Contacts lists them at once….

4.mp4

Test 5

With a saved Pubky identity: turn on airplane mode, force-stop Bitkit, open a pubkyauth:// link (Enable Payments in pubky.app), and turn airplane mode off within a few seconds →….

5.mp4

Test J1

Verifies that opening Profile straight after a relaunch, while the signed-in profile is still loading, shows the cached name read-only.

J1.mp4

Test J2

Verifies that the Pubky Ring choice screen lists every Ring identity straight away under its truncated key and fills each name in as its profile lookup….

J2.mp4

Test J3

A contact import keeps saving after you leave the import overview, and when it finishes it leaves you where you went instead of opening Pay Contacts.

J3.mp4

Test J4

Contacts shows the saved contacts as soon as it opens after a relaunch, under the names they were saved with, and fills in each contact's profile name and….

J4.mp4

Tip

Tests 1, 2, 3, 4, 5 worth a journey
Test 1
  • Open Profile from Home and tap Continue
  • See both Pubky Ring rows at once
  • See AdLovelace while the other row still shows Loading your profile
  • See the other row settle on its truncated key

Test 2

  • Tap the AdLovelace Ring row
  • See a spinner only on that row, in place of its key icon
  • See Pay Contacts open

Test 3

  • Open the Ring choice screen until both names are visible
  • Tap AdLovelace and turn airplane mode on
  • See both rows stay, with AdLovelace and the truncated key
  • Tap AdLovelace again and see only that row spin, then the names remain

Test 4

  • Adopt the published Ring identity
  • See the import screen count 16 friends
  • Import all of them
  • Open Contacts and see the published names and the unpublished keys
  • Save a photo and see it as the profile avatar

Test 5

  • Turn on airplane mode and force-stop Bitkit
  • Open a pubky auth link
  • Turn airplane mode off within a few seconds
  • See the approval sheet
  • Cold-start the same link and stay offline
  • See Couldn't Load Your Pubky Profile


Reviewed by grok-4.7-xhigh via gh-pr-review-loop skill
Commands: @ovi-reviewer review · test · retest (author)

@piotr-iohk piotr-iohk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Retested the current build on a Samsung with the same 61-follow Ring profile. Same result as the earlier device run.

The import list was ready in about 17 seconds. Continue still takes a few minutes on the seven unpublished follows, then stops. That wait is #1406 and belongs with #1401. Approving this for the profile-loading change.

@ovitrif ovitrif left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

utAck

@Jasonvdb
Jasonvdb merged commit b29956a into master Oct 3, 2026
22 checks passed
@Jasonvdb
Jasonvdb deleted the claude/flow-vibe-pubky-profile-load-lag branch October 3, 2026 05:14
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.

4 participants