Skip to content

Adopt Rust-driven shared state and drop duplicated Kotlin logic (#175)#178

Merged
kwsantiago merged 7 commits intomainfrom
rust-shared-state
Mar 2, 2026
Merged

Adopt Rust-driven shared state and drop duplicated Kotlin logic (#175)#178
kwsantiago merged 7 commits intomainfrom
rust-shared-state

Conversation

@kwsantiago
Copy link
Contributor

@kwsantiago kwsantiago commented Feb 28, 2026

Delete RelayConfigStore, ProxyConfigStore, ProfileRelayConfigStore, and BunkerConfigStore — config persistence now handled by Rust keep-mobile. Replace inline timestamp formatting with Rust formatTimestampDetailed() and all inline hex/npub truncation with Rust truncateStr(). Implement KeepStateCallback for push-based peers/connection state from Rust, removing the 10s polling loop for those values.

Summary by CodeRabbit

  • Refactoring
    • Consolidated configuration management system for relay, bunker, and proxy settings for improved state handling and maintainability.
    • Centralized text truncation utilities for consistent display formatting across the application.

Delete RelayConfigStore, ProxyConfigStore, ProfileRelayConfigStore, and
BunkerConfigStore — config persistence now handled by Rust keep-mobile.
Replace inline timestamp formatting with Rust formatTimestampDetailed()
and all inline hex/npub truncation with Rust truncateStr(). Implement
KeepStateCallback for push-based peers/connection state from Rust,
removing the 10s polling loop for those values.
@kwsantiago kwsantiago self-assigned this Feb 28, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

Warning

Rate limit exceeded

@kwsantiago has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 26 minutes and 44 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between f611cda and 0ddd657.

📒 Files selected for processing (15)
  • app/src/main/kotlin/io/privkey/keep/AccountActions.kt
  • app/src/main/kotlin/io/privkey/keep/AccountSwitcherSheet.kt
  • app/src/main/kotlin/io/privkey/keep/ImportNsecScreen.kt
  • app/src/main/kotlin/io/privkey/keep/ImportShareScreen.kt
  • app/src/main/kotlin/io/privkey/keep/KeepMobileApp.kt
  • app/src/main/kotlin/io/privkey/keep/NostrFormatting.kt
  • app/src/main/kotlin/io/privkey/keep/ShareDetailsScreen.kt
  • app/src/main/kotlin/io/privkey/keep/descriptor/WalletDescriptorScreen.kt
  • app/src/main/kotlin/io/privkey/keep/nip46/BunkerScreen.kt
  • app/src/main/kotlin/io/privkey/keep/nip46/Nip46ApprovalScreen.kt
  • app/src/main/kotlin/io/privkey/keep/nip46/NostrConnectActivity.kt
  • app/src/main/kotlin/io/privkey/keep/nip46/NostrConnectApprovalScreen.kt
  • app/src/main/kotlin/io/privkey/keep/nip55/AppPermissionsScreen.kt
  • app/src/main/kotlin/io/privkey/keep/nip55/Nip55ApprovalScreen.kt
  • app/src/main/kotlin/io/privkey/keep/nip55/PermissionsManagementScreen.kt

Walkthrough

The pull request removes four Kotlin storage classes (RelayConfigStore, ProfileRelayConfigStore, BunkerConfigStore, ProxyConfigStore) that persisted configuration locally, and migrates their functionality to KeepMobile uniffi-based APIs. Multiple UI screens and services are updated to access and persist config through KeepMobile instead of local stores. Additionally, string truncation/formatting logic is centralized via uniffi functions.

Changes

Cohort / File(s) Summary
Store Removal
app/src/main/kotlin/io/privkey/keep/storage/RelayConfigStore.kt, BunkerConfigStore.kt, ProfileRelayConfigStore.kt, ProxyConfigStore.kt, app/src/androidTest/kotlin/.../ProfileRelayConfigStoreTest.kt
Entire storage classes deleted (4 files, ~372 lines) that handled local persistence of relay, bunker, and proxy configuration via encrypted SharedPreferences. Related test file also removed.
Core App State Migration
app/src/main/kotlin/io/privkey/keep/KeepMobileApp.kt
Transitioned from config store dependencies to KeepMobile uniffi-based config access (getBunkerConfig, getRelayConfig, getProxyConfig); replaced MutableStateFlow-based state with KeepLiveState callback system; added getPinMismatch() accessor.
Account & Relay Actions
app/src/main/kotlin/io/privkey/keep/AccountActions.kt, ConnectionCards.kt
Updated AccountActions to source relay config from KeepMobile.getRelayConfig() instead of stores; changed relay validation in ConnectionCards to use RELAY_URL_REGEX and port validation utilities instead of RelayConfigStore.
Screen Config Access
app/src/main/kotlin/io/privkey/keep/MainActivity.kt, nip46/BunkerScreen.kt, nip46/BunkerService.kt, nip46/NostrConnectActivity.kt, nip55/AppPermissionsScreen.kt
Updated screens and services to read/write bunker, relay, and proxy config via KeepMobile methods; removed store parameters from composable signatures; updated bunker/client authorization flows to persist via KeepMobile.
Validation & Helper Utilities
app/src/main/kotlin/io/privkey/keep/RelayValidation.kt, SettingsCards.kt
New RelayValidation.kt file adds RELAY_URL_REGEX, port validation, and internal-host detection utilities; SettingsCards simplified port validation to direct range check.
Formatting Centralization
NostrFormatting.kt, SecurityCards.kt, ShareCards.kt, ShareDetailsScreen.kt, descriptor/WalletDescriptorScreen.kt, nip55/ConnectedAppsScreen.kt, nip55/SigningHistoryScreen.kt
Consolidated string truncation logic to use io.privkey.keep.uniffi.truncateStr and formatTimestampDetailed functions instead of inline substring/date-formatting operations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

Possibly related PRs

  • UI updates and settings improvements #139: Modifies ProfileRelayConfigStore, RelayConfigStore, AccountActions, MainActivity, and KeepMobileApp — the same components this PR removes/refactors, creating direct code-level overlap on per-account relay storage.
  • Split MainScreenCards and extract AccountActions #128: Updates AccountActions to depend on RelayConfigStore and ProfileRelayConfigStore — the exact stores this PR removes and migrates away from.
  • Add NIP-46 bunker mode #85: Adds and uses BunkerConfigStore and related NIP-46 bunker persistence — the same component this PR removes and replaces with KeepMobile-backed config.

Poem

🐰 Stores are gone, the uniffi calls,
KeepMobile now handles it all,
Truncated strings dance through the air,
No more local prefs to care!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main theme of the changeset: removing Kotlin config stores and adopting Rust-driven shared state management.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch rust-shared-state

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.

- Remove exception message from debug log to match consistent pattern
- Add @volatile to currentRelays for thread safety
- Add missing onDismiss() calls in delete non-active and switch error paths
- Move FFI calls off main thread in BunkerScreen toggle/revoke handlers
Copy link

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

🧹 Nitpick comments (2)
app/src/main/kotlin/io/privkey/keep/descriptor/WalletDescriptorScreen.kt (1)

49-50: Consider removing the fully-qualified name.

The wildcard import io.privkey.keep.uniffi.* at line 17 already imports truncateStr. The fully-qualified call is redundant.

Suggested simplification
 private fun truncateGroupPubkey(key: String): String =
-    io.privkey.keep.uniffi.truncateStr(key, 8u, 6u)
+    truncateStr(key, 8u, 6u)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/src/main/kotlin/io/privkey/keep/descriptor/WalletDescriptorScreen.kt`
around lines 49 - 50, The method truncateGroupPubkey uses a fully-qualified call
to io.privkey.keep.uniffi.truncateStr even though truncateStr is already
available via the wildcard import io.privkey.keep.uniffi.*; simplify by
replacing the fully-qualified call with a direct call to truncateStr(key, 8u,
6u) inside truncateGroupPubkey to remove redundancy and improve readability.
app/src/main/kotlin/io/privkey/keep/ShareDetailsScreen.kt (1)

96-99: Note: Visible prefix reduced from 24 to 12 characters.

The truncation parameters changed from showing 24 prefix characters to 12. For npub identification, the first 12 characters after "npub1" may provide less uniqueness for users to visually verify their key. Consider whether truncateStr(npub, 16u, 8u) or similar would better balance compactness with identifiability.

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

In `@app/src/main/kotlin/io/privkey/keep/ShareDetailsScreen.kt` around lines 96 -
99, The displayed npub prefix was shortened to 12 chars which may reduce user
identifiability; update the truncation call in ShareDetailsScreen (the Text that
currently uses io.privkey.keep.uniffi.truncateStr(npub, 12u, 8u)) to a larger
prefix such as truncateStr(npub, 16u, 8u) (or another agreed value like 24u,8u)
so the visible prefix balances compactness and recognizability; modify only the
parameters passed to truncateStr in that Text widget to the chosen prefix
length.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/kotlin/io/privkey/keep/KeepMobileApp.kt`:
- Around line 63-65: Replace the plain volatile property liveState:
KeepLiveState? with an observable state holder (e.g., a private
MutableStateFlow<KeepLiveState?> or MutableState<KeepLiveState?>) and expose it
as a read-only Flow/State (StateFlow or State) so callback-driven updates emit
to consumers; update all places that currently write to liveState (the setter
sites referenced around the callback handling code and where liveState is
assigned) to call emit/value assignment on the new observable (e.g.,
liveStateFlow.value = newState or liveStateMutable.value = newState), and in
Compose consumers collect it with collectAsStateWithLifecycle() (or
collectAsState()) instead of reading the plain property so UI recomposition
happens on updates.
- Around line 186-193: The updateBunkerService function currently attempts to
start/stop BunkerService regardless of whether mobile.saveBunkerConfig
succeeded; change it so the service is only started/stopped when the config
persist succeeds: call mobile.getBunkerConfig() then attempt
mobile.saveBunkerConfig(BunkerConfigInfo(enabled, current.authorizedClients))
and if that operation fails (handle the Result from runCatching or use
onSuccess/onFailure) log the error (as you already do with BuildConfig.DEBUG and
Log.e) and return early, otherwise proceed to invoke the selected action
(BunkerService::start or BunkerService::stop) with this.

In `@app/src/main/kotlin/io/privkey/keep/nip46/BunkerScreen.kt`:
- Around line 114-121: The saveBunkerRelays write path currently fires off
keepMobile.saveRelayConfig inside scope.launch without handling failures, so
wrap the save call in a suspend helper using runCatching (or try/catch) on the
IO dispatcher, show explicit UI failure feedback if it fails, and only update UI
state variables (relays / authorizedClients) after the save returns success;
specifically update the logic around saveBunkerRelays (and the similar blocks at
the other mentioned locations) to call a shared suspend helper that invokes
keepMobile.saveRelayConfig, checks the Result, posts an error message to the UI
on failure, and only commits the new relay/authorization state on success.
- Around line 97-101: The Compose code in BunkerScreen is directly calling
keepMobile.getBunkerConfig() and keepMobile.getRelayConfig(null) inside remember
(creating bunkerConfig and relays) which can block or throw; change this to
initialize bunkerConfig and relays as safe, empty/default Compose state and then
load the actual values inside a LaunchedEffect(Dispatchers.IO) using runCatching
to call keepMobile.getBunkerConfig() and keepMobile.getRelayConfig(null); on
success update the Compose state (bunkerConfig and relays) on the main thread,
and on failure log or handle the error so composition cannot crash or freeze.

In `@app/src/main/kotlin/io/privkey/keep/nip46/NostrConnectActivity.kt`:
- Around line 217-220: The current filter chain in NostrConnectActivity calls
isInternalHost(url) on the main thread (inside the .filter { ... } lambda during
onCreate), which does DNS resolution and can block the UI; refactor so only the
cheap checks (url.startsWith("wss://") and RELAY_URL_REGEX.matches(url)) run on
the main thread, and perform the isInternalHost(url) call off the main thread
(e.g., wrap the internal-host check in withContext(Dispatchers.IO) or run the
full host-checking filter in a coroutine/Flow using Dispatchers.IO) so URI
parsing and relay internal-host filtering happen asynchronously before updating
UI. Ensure you update the code paths that consume this filtered list to be
suspendable/async if needed and keep references to RELAY_URL_REGEX and
isInternalHost unchanged.

In `@app/src/main/kotlin/io/privkey/keep/nip55/AppPermissionsScreen.kt`:
- Around line 369-376: The current runCatching block around
getKeepMobile()/getBunkerConfig()/saveBunkerConfig(...) swallows failures so the
UI may report success while the pubkey remains in authorizedClients; change it
to explicitly capture the save result and surface errors: call
(context.applicationContext as? KeepMobileApp)?.getKeepMobile(), read config via
getBunkerConfig(), compute updated list, then call
saveBunkerConfig(BunkerConfigInfo(...)) and check its Boolean return value — if
it returns false or an exception occurs, propagate or report the error to the
caller/UI instead of ignoring it (use the same symbols: getKeepMobile,
getBunkerConfig, saveBunkerConfig, BunkerConfigInfo).

In `@app/src/main/kotlin/io/privkey/keep/RelayValidation.kt`:
- Around line 18-28: isInternalHost currently performs synchronous DNS lookup
with InetAddress.getAllByName(host) and can block the main thread when called
from NostrConnectActivity.parseNostrConnectUri() in onCreate; make the DNS work
asynchronous by turning isInternalHost into a suspend function (or providing an
isInternalHostAsync) and perform InetAddress.getAllByName(host) inside
withContext(Dispatchers.IO), then update callers (e.g., parseNostrConnectUri in
NostrConnectActivity) to call the suspend version from a coroutine (or use
lifecycleScope) so DNS resolution never runs on the UI thread.

---

Nitpick comments:
In `@app/src/main/kotlin/io/privkey/keep/descriptor/WalletDescriptorScreen.kt`:
- Around line 49-50: The method truncateGroupPubkey uses a fully-qualified call
to io.privkey.keep.uniffi.truncateStr even though truncateStr is already
available via the wildcard import io.privkey.keep.uniffi.*; simplify by
replacing the fully-qualified call with a direct call to truncateStr(key, 8u,
6u) inside truncateGroupPubkey to remove redundancy and improve readability.

In `@app/src/main/kotlin/io/privkey/keep/ShareDetailsScreen.kt`:
- Around line 96-99: The displayed npub prefix was shortened to 12 chars which
may reduce user identifiability; update the truncation call in
ShareDetailsScreen (the Text that currently uses
io.privkey.keep.uniffi.truncateStr(npub, 12u, 8u)) to a larger prefix such as
truncateStr(npub, 16u, 8u) (or another agreed value like 24u,8u) so the visible
prefix balances compactness and recognizability; modify only the parameters
passed to truncateStr in that Text widget to the chosen prefix length.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16889a9 and f611cda.

📒 Files selected for processing (22)
  • app/src/androidTest/kotlin/io/privkey/keep/storage/ProfileRelayConfigStoreTest.kt
  • app/src/main/kotlin/io/privkey/keep/AccountActions.kt
  • app/src/main/kotlin/io/privkey/keep/ConnectionCards.kt
  • app/src/main/kotlin/io/privkey/keep/KeepMobileApp.kt
  • app/src/main/kotlin/io/privkey/keep/MainActivity.kt
  • app/src/main/kotlin/io/privkey/keep/NostrFormatting.kt
  • app/src/main/kotlin/io/privkey/keep/RelayValidation.kt
  • app/src/main/kotlin/io/privkey/keep/SecurityCards.kt
  • app/src/main/kotlin/io/privkey/keep/SettingsCards.kt
  • app/src/main/kotlin/io/privkey/keep/ShareCards.kt
  • app/src/main/kotlin/io/privkey/keep/ShareDetailsScreen.kt
  • app/src/main/kotlin/io/privkey/keep/descriptor/WalletDescriptorScreen.kt
  • app/src/main/kotlin/io/privkey/keep/nip46/BunkerScreen.kt
  • app/src/main/kotlin/io/privkey/keep/nip46/BunkerService.kt
  • app/src/main/kotlin/io/privkey/keep/nip46/NostrConnectActivity.kt
  • app/src/main/kotlin/io/privkey/keep/nip55/AppPermissionsScreen.kt
  • app/src/main/kotlin/io/privkey/keep/nip55/ConnectedAppsScreen.kt
  • app/src/main/kotlin/io/privkey/keep/nip55/SigningHistoryScreen.kt
  • app/src/main/kotlin/io/privkey/keep/storage/BunkerConfigStore.kt
  • app/src/main/kotlin/io/privkey/keep/storage/ProfileRelayConfigStore.kt
  • app/src/main/kotlin/io/privkey/keep/storage/ProxyConfigStore.kt
  • app/src/main/kotlin/io/privkey/keep/storage/RelayConfigStore.kt
💤 Files with no reviewable changes (5)
  • app/src/androidTest/kotlin/io/privkey/keep/storage/ProfileRelayConfigStoreTest.kt
  • app/src/main/kotlin/io/privkey/keep/storage/ProxyConfigStore.kt
  • app/src/main/kotlin/io/privkey/keep/storage/ProfileRelayConfigStore.kt
  • app/src/main/kotlin/io/privkey/keep/storage/RelayConfigStore.kt
  • app/src/main/kotlin/io/privkey/keep/storage/BunkerConfigStore.kt

- Add @volatile to pinMismatch for thread safety
- Flatten nested withContext in BunkerScreen revoke handler
- Preserve authorized clients when disabling bunker on account switch
- Use mutableStateOf + main-thread hop for liveState per RMP pattern
- Early return in updateBunkerService when config save fails
- Move BunkerScreen FFI init to LaunchedEffect(Dispatchers.IO)
- Add error handling to saveBunkerRelays and toggle/revoke handlers
- Move isInternalHost off main thread in NostrConnectActivity
- Add failure logging to revokeNip46Client
- Remove FQ truncateStr call in WalletDescriptorScreen
- Use formatTimestampDetailed() in WalletDescriptorScreen and PermissionsManagementScreen
- Remove startPeriodicPeerCheck and announceJob (replaced by push-based liveState)
Switch all callers to UniFFI-exposed functions from keep-mobile:
- formatPubkeyDisplay, formatEventIdDisplay, hexToNpub
- isHex64, isValidNsecFormat, nsecToHex, isValidBech32Char

Depends on privkeyio/keep expose-nostr-formatting-uniffi branch.
@kwsantiago kwsantiago merged commit a4517ac into main Mar 2, 2026
3 of 5 checks passed
@kwsantiago kwsantiago deleted the rust-shared-state branch March 2, 2026 14:02
@kwsantiago kwsantiago linked an issue Mar 2, 2026 that may be closed by this pull request
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.

Adopt Rust-driven shared state and drop duplicated Kotlin logic

1 participant