Skip to content

Add wallet reordering to sidebar (iOS + Android)#688

Open
diyaayay wants to merge 12 commits intobitcoinppl:masterfrom
diyaayay:cove-sort-wallets-hamburger-menu
Open

Add wallet reordering to sidebar (iOS + Android)#688
diyaayay wants to merge 12 commits intobitcoinppl:masterfrom
diyaayay:cove-sort-wallets-hamburger-menu

Conversation

@diyaayay
Copy link
Copy Markdown
Contributor

@diyaayay diyaayay commented Apr 19, 2026

Summary

Adds wallet reordering in the hamburger menu (closes #493).

  • Rust: position: u32 on WalletMetadata, stable-sorted get_all(), one-time migrate_positions() backfill on startup.
  • UniFFI: reorderWallets(orderedIds) and moveWallet(walletId, toPosition); new WalletTableError::{WalletNotFound, ReorderMismatch}.
  • iOS: drag-to-reorder in SidebarView using List + .onMove + EditMode; haptics on drag start and on successful persist; optimistic update with rollback on FFI error; edit-mode resets when the sidebar closes.
  • Android: long-press drag-to-reorder in SidebarView.kt; haptics; visual elevation (scale + shadow + zIndex); smooth animateItem() for neighbors; FFI persisted off the UI thread; optimistic update with rollback on error.
  • Bindings: regenerated Kotlin and Swift UniFFI bindings.

Testing

  • cargo fmt --check — pass
  • cargo clippy --all-features -- -D warnings — pass
  • cargo test --package cove --lib — passes aside from pre-existing database::migration::bdk / database::encrypted_backend flakiness (reproduced on master; unrelated to this PR)
  • just ba — Android build + Kotlin bindings regenerated successfully
  • just bi — iOS build + Swift bindings regenerated successfully

Manual: reordered wallets on an Android device (Pixel) and on the iOS simulator; killed and reopened the app — wallet order persisted in the new position on both.

Platform Coverage

  • Tested on iOS device
  • Tested on Android device
  • Tested on iOS simulator
  • Tested on Android simulator
  • Not tested

Checklist

Summary by CodeRabbit

  • New Features

    • Android: long-press drag-and-drop to reorder wallets in the sidebar.
    • iOS: “Edit Order” mode with drag-reorder, Done control, and contextual edit action.
  • Improvements

    • Ordering is now persisted and consistently applied across devices; background migration repairs positions.
    • Smoother visuals, elevation/translation effects and enhanced haptic feedback during reordering.
  • Bug Fixes

    • Optimistic reorders now rollback or reconcile if persistence fails, with reload on mismatch.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 19, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds in‑view wallet reordering on Android and iOS, persists ordering via new UniFFI Rust APIs, introduces a persistent position field in wallet metadata with migration and validation, and implements database move/reorder semantics and error variants.

Changes

Cohort / File(s) Summary
Android UI Drag-to-Reorder
android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt
Implements long-press drag-and-drop using an optimistic keyed walletList, per-item height measurement, pointer drag handling, visual transforms (translation/scale/elevation/zIndex), disables item clicks during drag, swaps neighbors past threshold, and persists via async persistReorder with rollback on mismatch/failure.
iOS UI Reordering with Edit Mode
ios/Cove/Views/SidebarView.swift
Adds editMode, reorderable List with .onMove, "Edit Order" context action and haptics, optimistic local reorders, detached async persistence via database.wallets().reorderWallets, and guarded adoption of persisted ordering (checks ID sequence).
Kotlin UniFFI Bindings & API
android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
Adds UniFFI exports & checksum checks for walletstable_move_wallet and walletstable_reorder_wallets; exposes WalletsTable.moveWallet and reorderWallets; adds position: UInt to WalletMetadata; and adds WalletNotFound / ReorderMismatch error variants.
Rust DB: Wallet Table APIs & Semantics
rust/src/database/wallet.rs
Adds reorder_wallets, move_wallet, and migrate_positions; validates permutations, clamps move indices, rewrites contiguous position values, persists in single txn via mutate_positions_txn, emits Update::WalletsChanged, appends new wallets with max+1 position, and renumbers on delete.
Rust Wallet Metadata
rust/src/wallet/metadata.rs
Adds public position: u32 (serde default) and sets position = 0 in new and preview_new constructors.
Rust Database Init
rust/src/database.rs
Performs best-effort wallets.migrate_positions() after initial commit and logs errors without failing Database::init.
Rust Backup Test Update
rust/src/backup/model.rs
Test fixture updated to include position: 0 for WalletMetadata in unit test.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as SidebarView (Android / iOS)
    participant App as App State
    participant FFI as UniFFI Rust FFI
    participant DB as Database

    User->>UI: Initiate reorder (long-press drag / enter edit + drag)
    UI->>App: Apply optimistic local reorder (walletList / app.wallets)
    App->>UI: Render updated order and drag visuals

    User->>UI: Continue drag / drop to target
    UI->>App: Update visual swaps and track offsets

    User->>UI: Release / end drag
    UI->>FFI: Call moveWallet(toPosition) or reorderWallets(orderedIds)
    FFI->>DB: Validate, mutate positions, persist
    DB-->>FFI: Success or Error

    alt Success
        FFI-->>UI: Return updated metadata list
        UI->>App: Confirm persisted order, clear drag/edit state
    else Failure
        FFI-->>UI: Return error
        UI->>App: Roll back to pre-drag snapshot, clear drag/edit state
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 I hop and hold a wallet tight,

I nudge it up, I nudge it right,
Rust numbers order, UI takes flight,
Haptics clap — the list sits tight,
A tidy row beneath moonlight.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'Add wallet reordering to sidebar (iOS + Android)' is concise, specific, and clearly describes the primary feature added—wallet reordering across both platforms.
Description check ✅ Passed The description covers required sections: Summary (feature overview and technical scope across Rust, UniFFI, iOS, Android, and bindings), Testing (both automated checks and manual platform verification with platform coverage checkboxes), and Checklist (confirms review of contributing and architecture docs).
Linked Issues check ✅ Passed The PR fully satisfies the objectives from linked issue #493: implements drag-to-reorder wallets in the sidebar on both iOS and Android, provides persistent storage across app restarts, and delivers an intuitive touch-based interaction.
Out of Scope Changes check ✅ Passed All changes are directly aligned with wallet reordering requirements: Rust position tracking and sorting, UniFFI reorder APIs, iOS/Android UI interactions, and supporting infrastructure (bindings, migrations, error handling). No unrelated or extraneous changes detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@diyaayay diyaayay force-pushed the cove-sort-wallets-hamburger-menu branch from d39ec60 to a077c6e Compare April 20, 2026 15:35
@diyaayay diyaayay marked this pull request as ready for review April 20, 2026 17:08
@diyaayay diyaayay force-pushed the cove-sort-wallets-hamburger-menu branch from 107c171 to ce03c4a Compare April 20, 2026 17:12
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 20, 2026

Greptile Summary

Adds drag-to-reorder wallet ordering via a new position: u32 field on WalletMetadata, a one-time startup migration, and reorderWallets / moveWallet UniFFI APIs, with optimistic-update + rollback UX on both iOS (SwiftUI List .onMove) and Android (long-press detectDragGesturesAfterLongPress).

A few correctness issues in the Rust layer are worth addressing before shipping:

  • delete() does not re-compact positions after removal, so the next save_new_wallet_metadata call assigns a position that duplicates an existing one.
  • update_wallet_metadata unconditionally overwrites position, making any settings-save with a slightly stale metadata object silently undo a drag reorder.
  • The migrate_positions all-zeros heuristic is bypassed when a mix of zero and non-zero positions already exists.

Confidence Score: 4/5

Safe to merge with awareness of three P2 position-consistency issues in the Rust layer that could produce subtle ordering regressions in specific user journeys.

All four findings are P2 — no data loss, no crashes, no security issues. The stable sort and the self-correcting nature of reorder_wallets mean visible bugs are rare in practice. However, the update_wallet_metadata position-clobber and delete-then-add position collision are real latent correctness bugs that will become more apparent as the feature is used.

rust/src/database/wallet.rs — position lifecycle (delete compaction, update preservation, migration heuristic)

Important Files Changed

Filename Overview
rust/src/database/wallet.rs Core reorder logic added (reorder_wallets, move_wallet, migrate_positions); three latent correctness issues: positions not re-compacted on delete, all-zeros migration heuristic bypassed on mixed-position state, and update_wallet_metadata overwrites position.
rust/src/wallet/metadata.rs Adds position: u32 field with #[serde(default)]; backward-compatible, initialised to 0 in all constructors.
rust/src/database.rs Calls migrate_positions on startup with error logging; straightforward wiring.
ios/Cove/Views/SidebarView.swift Drag-to-reorder via SwiftUI List + .onMove; optimistic update, haptics, edit-mode reset on sidebar close; rollback on FFI failure looks correct.
android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt Long-press drag-to-reorder with visual elevation, animateItem neighbors, IO-thread FFI persist, and optimistic rollback; item height captured only on first measurement which may cause imprecise drag thresholds.
rust/src/backup/model.rs No functional changes; new test fixture adds position: 0 to WalletMetadata inline construction to satisfy the new required field.

Sequence Diagram

sequenceDiagram
    participant UI as iOS/Android UI
    participant App as AppManager (wallets state)
    participant FFI as WalletsTable (Rust FFI)
    participant DB as redb Database

    UI->>App: read app.wallets (snapshot = previous)
    UI->>App: optimistic update app.wallets = reordered
    UI->>UI: haptic feedback (drag start)
    UI->>FFI: reorderWallets(orderedIds) [background thread]
    FFI->>DB: get_all() → sort_by_key(position)
    FFI->>FFI: validate ordered_ids is a permutation
    FFI->>DB: save_all_wallets() with new positions
    FFI-->>UI: send WalletsChanged update
    App->>App: app.wallets refreshed from DB

    alt persist succeeds
        UI->>UI: haptic success
    else persist fails
        UI->>App: rollback app.wallets = previous
    end
Loading

Comments Outside Diff (2)

  1. rust/src/database/wallet.rs, line 384-396 (link)

    P2 Position collision after delete + add

    delete removes a wallet but leaves the remaining positions with a gap (e.g., [0, 2] after removing position 1). When the next wallet is added, wallet.position = wallets.len() as u32 (line 217) produces 2 again, resulting in two wallets at the same position. Rust's stable sort_by_key preserves their stored order for display, so the UI stays consistent, but the position values are silently inconsistent and any subsequent update_wallet_metadata that writes back the stale position will cement the confusion.

    Adding a position re-compaction pass after retain would prevent this:

    wallets.retain(|wallet| &wallet.id != id);
    for (i, w) in wallets.iter_mut().enumerate() {
        w.position = i as u32;
    }
    self.save_all_wallets(network, mode, wallets)?;
  2. rust/src/database/wallet.rs, line 329-344 (link)

    P2 update_wallet_metadata silently overwrites position

    update_wallet_metadata replaces the entire persisted record including position. Any caller that reads a WalletMetadata before a reorder and writes it back after (e.g., a wallet-settings save that is in-flight while the user is also drag-reordering) will silently revert the position to the pre-reorder value. Position is now a meaningful ordering field, so callers with a stale copy can corrupt the user-visible order without any error.

    Consider preserving the persisted position on update:

    for wallet in &mut wallets {
        if wallet.id == metadata.id {
            let current_position = wallet.position; // preserve DB position
            *wallet = metadata.clone();
            wallet.position = current_position;
        }
    }

Reviews (1): Last reviewed commit: "Polish iOS sidebar reorder:success hapti..." | Re-trigger Greptile

Comment thread rust/src/database/wallet.rs
Comment thread android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt Outdated
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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/src/database/wallet.rs (1)

210-220: ⚠️ Potential issue | 🟠 Major

Normalize positions before assigning the new wallet’s position.

wallets.len() only appends correctly when existing positions are contiguous. After deleting middle wallets, positions can have gaps; a new wallet can then receive a lower position than an existing wallet and sort above it instead of appearing at the end.

Proposed fix
         let mut wallet = wallet;
+        for (i, existing_wallet) in wallets.iter_mut().enumerate() {
+            existing_wallet.position = i as u32;
+        }
         wallet.position = wallets.len() as u32;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/database/wallet.rs` around lines 210 - 220, The code currently sets
wallet.position = wallets.len() which fails when positions have gaps; modify the
insertion logic in the function using get_all and wallets to first
normalize/reindex existing wallets' positions (e.g., sort wallets by position
and assign positions 0..N-1) or compute the current maximum position and set the
new wallet's position to max_position + 1, then proceed with
should_backup_to_cloud cloning and wallets.push; ensure any normalization
updates the persisted records or is reflected before using wallets elsewhere so
ordering remains correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@rust/src/database.rs`:
- Around line 141-143: Database::init currently swallows failures from
wallets.migrate_positions() by logging and continuing; change it to propagate
the error so init fails on migration errors. Replace the if-let that calls
wallets.migrate_positions() so that any Err(err) is returned (converted into the
function's Error/Result type) instead of only logging; reference the
Database::init function and the wallets.migrate_positions() call to locate the
change and ensure the returned error preserves the original err message/context.

In `@rust/src/database/wallet.rs`:
- Around line 136-195: reorder_wallets and move_wallet currently read wallets in
a read transaction then later call save_all_wallets with a cloned vector, which
can overwrite newer metadata written between read and save; change both
functions to perform the read→modify→write inside a single write transaction (or
implement and call a position-only updater that re-reads the latest wallets and
patchs only the position fields) so that you either use the DB write transaction
around get_all + modifications + save_all_wallets, or re-fetch the current
wallet list just before persisting and only update positions (referencing
reorder_wallets, move_wallet, and save_all_wallets/position update helper).

---

Outside diff comments:
In `@rust/src/database/wallet.rs`:
- Around line 210-220: The code currently sets wallet.position = wallets.len()
which fails when positions have gaps; modify the insertion logic in the function
using get_all and wallets to first normalize/reindex existing wallets' positions
(e.g., sort wallets by position and assign positions 0..N-1) or compute the
current maximum position and set the new wallet's position to max_position + 1,
then proceed with should_backup_to_cloud cloning and wallets.push; ensure any
normalization updates the persisted records or is reflected before using wallets
elsewhere so ordering remains correct.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 33b4b351-bf12-4605-b31c-5d60487caf7c

📥 Commits

Reviewing files that changed from the base of the PR and between 8766f7d and 107c171.

⛔ Files ignored due to path filters (1)
  • ios/CoveCore/Sources/CoveCore/generated/cove.swift is excluded by !**/generated/**
📒 Files selected for processing (7)
  • android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt
  • android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
  • ios/Cove/Views/SidebarView.swift
  • rust/src/backup/model.rs
  • rust/src/database.rs
  • rust/src/database/wallet.rs
  • rust/src/wallet/metadata.rs

Comment thread rust/src/database.rs
Comment thread rust/src/database/wallet.rs
@diyaayay diyaayay force-pushed the cove-sort-wallets-hamburger-menu branch from a8e441c to 5e380b7 Compare April 20, 2026 18:27
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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/Cove/Views/SidebarView.swift`:
- Around line 178-203: The persistWalletOrder flow is vulnerable to out-of-order
completions: update persistWalletOrder and handleMove so writes are serialized
or single-flight and success/rollback only apply if the current UI still matches
that request’s optimistic order; specifically, in persistWalletOrder use a
request token or compare appRef.wallets.map(\.id) to the orderedIds before doing
await MainActor.run updates (both success haptic and rollback), and on success
consume the returned [WalletMetadata] from
database.wallets().reorderWallets(...) to update appRef.wallets (positions)
instead of discarding it; also introduce a simple in-flight guard (e.g., a
single-flight Bool or serial Task queue) to prevent starting a new detach while
one is pending or to enqueue subsequent moves so persistence is serialized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2645647e-39b7-4ecb-bb19-60c927441e1b

📥 Commits

Reviewing files that changed from the base of the PR and between 107c171 and 5e380b7.

⛔ Files ignored due to path filters (1)
  • ios/CoveCore/Sources/CoveCore/generated/cove.swift is excluded by !**/generated/**
📒 Files selected for processing (7)
  • android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt
  • android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
  • ios/Cove/Views/SidebarView.swift
  • rust/src/backup/model.rs
  • rust/src/database.rs
  • rust/src/database/wallet.rs
  • rust/src/wallet/metadata.rs
✅ Files skipped from review due to trivial changes (2)
  • rust/src/backup/model.rs
  • rust/src/database.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt
  • rust/src/database/wallet.rs

Comment thread ios/Cove/Views/SidebarView.swift
@diyaayay diyaayay force-pushed the cove-sort-wallets-hamburger-menu branch from 5e380b7 to e225e62 Compare April 20, 2026 18:34
@diyaayay diyaayay changed the title Cove sort wallets hamburger menu Add wallet reordering to sidebar (iOS + Android) Apr 21, 2026
@diyaayay diyaayay force-pushed the cove-sort-wallets-hamburger-menu branch from 81a3428 to c1ba778 Compare April 21, 2026 06:04
Copy link
Copy Markdown
Contributor

@praveenperera praveenperera left a comment

Choose a reason for hiding this comment

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

Serialize Android reorder writes so older drags can't win

If a user performs another reorder before the previous reorderWallets(...) call finishes, each drag launches an independent coroutine here and whichever write completes last becomes the persisted order. That means an older drag can overwrite the user's newer order even though the UI already showed the later arrangement. iOS already avoids this by chaining reorderTask, and Android needs the same serialization or version check.

@Sandipmandal25
Copy link
Copy Markdown
Collaborator

Sandipmandal25 commented Apr 21, 2026

Hey @diyaayay i have just a thought maybe i am wrong you can correct me but can we do the commit history bit more clean? like for example some commits are patching earlier commits in the same pr and the binding regeneration commits are floating separately when they should be squashed into the feature commits that triggered them. can we make the repo clean using 3 4 logical commits. What do you think?
cc @praveenperera

@praveenperera
Copy link
Copy Markdown
Contributor

praveenperera commented Apr 21, 2026

@Sandipmandal25 its alright, i'm just going to do a squash merge, I do prefer focused logical commits, but doing a squash merge is easier than trying to enforce this on each PR. Also having these messy commits make it easier to see each change that was made after a review.

@Sandipmandal25
Copy link
Copy Markdown
Collaborator

@Sandipmandal25 its alright, i'm just going to do a squash merge

makes sense thanks!

@diyaayay diyaayay force-pushed the cove-sort-wallets-hamburger-menu branch from c1ba778 to 54cf710 Compare April 21, 2026 18:38
@diyaayay
Copy link
Copy Markdown
Contributor Author

Serialize Android reorder writes so older drags can't win

If a user performs another reorder before the previous reorderWallets(...) call finishes, each drag launches an independent coroutine here and whichever write completes last becomes the persisted order. That means an older drag can overwrite the user's newer order even though the UI already showed the later arrangement. iOS already avoids this by chaining reorderTask, and Android needs the same serialization or version check.

Persists are now serialized under a Mutex and a post-FFI id-equality guard skips stale success/rollback; on success the UI adopts the authoritative List<WalletMetadata> from Rust. Android parity with iOS's reorderTask chain.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
rust/src/database/wallet.rs (1)

210-222: ⚠️ Potential issue | 🟡 Minor

Race: concurrent new-wallet inserts can mint duplicate position values.

get_all runs in a read transaction and save_all_wallets opens its own write transaction, so two concurrent save_new_wallet_metadata_with_backup_behavior calls (or a concurrent scan/metadata update that adds a wallet) can both observe the same max and both write position = max + 1. migrate_positions() will heal this on the next startup, but in the current session get_all's stable sort will leave the tie in insertion order, which can still produce surprising UI ordering until restart.

Consider moving the read + max-computation + write into a single write transaction (a small insert_new_wallet_txn helper mirroring mutate_positions_txn) so position uniqueness is guaranteed per insert.

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

In `@rust/src/database/wallet.rs` around lines 210 - 222, The current flow in
save_new_wallet_metadata_with_backup_behavior uses get_all (read transaction) to
compute max position and then calls save_all_wallets (write transaction), which
allows race conditions producing duplicate wallet.position; move the
read+max+insert into a single write transaction by creating an
insert_new_wallet_txn helper (similar to mutate_positions_txn) that opens a
write txn, reads existing wallets, computes max position, assigns
wallet.position = max+1, checks for duplicate id, appends the new wallet, and
saves all wallets in that same txn; update
save_new_wallet_metadata_with_backup_behavior to call insert_new_wallet_txn (and
keep migrate_positions/mutate_positions_txn untouched).
♻️ Duplicate comments (1)
ios/Cove/Views/SidebarView.swift (1)

192-218: ⚠️ Potential issue | 🟠 Major

Rollback can still land on an unpersisted optimistic order.

Queued writes are serialized, but previous is not guaranteed to be the last persisted state. If an earlier queued reorder fails as stale and a later one also fails, the later catch can restore the earlier optimistic order even though it never reached the database. Keep a confirmed authoritative baseline, refetch on failure, or make reorder persistence single-flight; also trigger success haptics only inside the same “still current” guard.

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

In `@ios/Cove/Views/SidebarView.swift` around lines 192 - 218, persistWalletOrder
currently restores the optimistic `previous` state on any failure which can
reapply an order that was never persisted; change the error-path to re-sync with
the authoritative persisted state instead of blindly setting `appRef.wallets =
previous`: in the catch for Task in persistWalletOrder call the persistence
layer (database.wallets().list or similar) to fetch the current persisted wallet
order and assign that to appRef.wallets only if appRef.wallets.map(\.id) ==
orderedIds (i.e. still matches the optimistic order), and move
HapticFeedback.success.trigger() inside the same MainActor guard that checks the
appRef.wallets ids before assigning `persisted` so haptics only fire when the
persisted result is still current; alternatively make reorderTask single-flight
so only the latest reorder is applied.
🧹 Nitpick comments (2)
rust/src/database/wallet.rs (2)

320-328: Nit: inconsistent empty-vec fallback.

get_all uses .unwrap_or(vec![]) (line 324) while the new mutate_positions_txn uses .unwrap_or_default() (line 458). Prefer unwrap_or_default() in both for consistency and to avoid the small eager allocation on the hot path.

♻️ Proposed tweak
-            .unwrap_or(vec![]);
+            .unwrap_or_default();

Also applies to: 454-458

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

In `@rust/src/database/wallet.rs` around lines 320 - 328, In get_all and
mutate_positions_txn in wallet.rs replace the eager fallback .unwrap_or(vec![])
with .unwrap_or_default() so the empty Vec<WalletMetadata> uses the default
allocation-free constructor; locate the
.get(key.as_str()).map_err_str(WalletTableError::ReadError)?...map(|value|
value.value()).unwrap_or(vec![]) in the get_all implementation and the analogous
unwrap in mutate_positions_txn and switch both to .unwrap_or_default().

239-276: migrate_positions only fires on duplicate positions, not on gaps.

The duplicate-only trigger is a reasonable scope for this migration, but note that pre-existing non-contiguous positions (e.g., [0, 2, 5] from a partially applied older build) won't be normalized. Functionally fine because get_all sorts by position, and the insertion path uses max + 1 so gaps don't cause collisions — just flagging so it's an intentional decision. If you'd rather normalize aggressively, check wallets.iter().enumerate().any(|(i, w)| w.position as usize != i) instead of just duplicates.

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

In `@rust/src/database/wallet.rs` around lines 239 - 276, The migration currently
only runs when duplicates are found; to also normalize non-contiguous positions
(gaps) update the trigger in migrate_positions to detect any wallet whose
position does not match its sorted index: replace the duplicate check that uses
seen.insert on wallets.iter() with an enumerate-based check like
wallets.iter().enumerate().any(|(i, w)| w.position as usize != i) and then
proceed to rebuild migrated from wallets and call self.save_all_wallets(network,
mode, migrated) as before; keep using get_all(network, mode) which returns
wallets sorted by position.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt`:
- Around line 190-194: The rollback restores the already-mutated optimistic
order because `previous` is captured after `walletList` is changed; fix by
capturing the pre-drag snapshot of the wallet order when the drag begins (e.g.,
in the `onDragStart` handler) and store it in a dedicated variable (like
`previousWalletOrder` or reuse `previous`) before any reordering occurs, then
use that snapshot in `onRollback` to restore the true pre-drag state; update
references to `walletList`, `onDragStart`, `draggedWalletId`, `draggedOffsetY`,
and `onRollback` accordingly so the captured order is taken prior to mutation.
- Around line 58-62: The Rust FFI call reorderWallets() in SidebarView.kt is
synchronous and should not be wrapped in withContext(Dispatchers.IO); remove the
withContext(Dispatchers.IO) wrapper around reorderWallets() in the coroutine
blocks (the launch + withContext sequence that currently calls reorderWallets())
and call reorderWallets() directly inside the existing coroutine or protected
Mutex.withLock block (same change for the second occurrence referenced around
the file). Ensure you keep the surrounding coroutine scope, Mutex locking
(Mutex.withLock), and error handling intact while eliminating the extra
Dispatchers.IO context switch.
- Around line 337-340: The reorderWallets success path currently calls
onAdoptAuthoritative which only mutates the local walletList, leaving
app.wallets stale until a Rust-originated WalletsChanged reconciliation arrives;
to avoid temporary ordering inconsistencies, after
onAdoptAuthoritative(authoritative) succeeds (inside the result.onSuccess block
where haptics.performHapticFeedback is called), either invoke app.loadWallets()
to immediately refresh the shared app.wallets state or ensure reorderWallets
emits a WalletsChanged event that is processed before any sidebar
recreation—update the code in SidebarView.kt to call app.loadWallets() after
onAdoptAuthoritative or add confirmation that WalletsChanged is synchronously
triggered and handled.

---

Outside diff comments:
In `@rust/src/database/wallet.rs`:
- Around line 210-222: The current flow in
save_new_wallet_metadata_with_backup_behavior uses get_all (read transaction) to
compute max position and then calls save_all_wallets (write transaction), which
allows race conditions producing duplicate wallet.position; move the
read+max+insert into a single write transaction by creating an
insert_new_wallet_txn helper (similar to mutate_positions_txn) that opens a
write txn, reads existing wallets, computes max position, assigns
wallet.position = max+1, checks for duplicate id, appends the new wallet, and
saves all wallets in that same txn; update
save_new_wallet_metadata_with_backup_behavior to call insert_new_wallet_txn (and
keep migrate_positions/mutate_positions_txn untouched).

---

Duplicate comments:
In `@ios/Cove/Views/SidebarView.swift`:
- Around line 192-218: persistWalletOrder currently restores the optimistic
`previous` state on any failure which can reapply an order that was never
persisted; change the error-path to re-sync with the authoritative persisted
state instead of blindly setting `appRef.wallets = previous`: in the catch for
Task in persistWalletOrder call the persistence layer (database.wallets().list
or similar) to fetch the current persisted wallet order and assign that to
appRef.wallets only if appRef.wallets.map(\.id) == orderedIds (i.e. still
matches the optimistic order), and move HapticFeedback.success.trigger() inside
the same MainActor guard that checks the appRef.wallets ids before assigning
`persisted` so haptics only fire when the persisted result is still current;
alternatively make reorderTask single-flight so only the latest reorder is
applied.

---

Nitpick comments:
In `@rust/src/database/wallet.rs`:
- Around line 320-328: In get_all and mutate_positions_txn in wallet.rs replace
the eager fallback .unwrap_or(vec![]) with .unwrap_or_default() so the empty
Vec<WalletMetadata> uses the default allocation-free constructor; locate the
.get(key.as_str()).map_err_str(WalletTableError::ReadError)?...map(|value|
value.value()).unwrap_or(vec![]) in the get_all implementation and the analogous
unwrap in mutate_positions_txn and switch both to .unwrap_or_default().
- Around line 239-276: The migration currently only runs when duplicates are
found; to also normalize non-contiguous positions (gaps) update the trigger in
migrate_positions to detect any wallet whose position does not match its sorted
index: replace the duplicate check that uses seen.insert on wallets.iter() with
an enumerate-based check like wallets.iter().enumerate().any(|(i, w)| w.position
as usize != i) and then proceed to rebuild migrated from wallets and call
self.save_all_wallets(network, mode, migrated) as before; keep using
get_all(network, mode) which returns wallets sorted by position.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8867c58f-dbaa-4e34-ae92-97204b8fc038

📥 Commits

Reviewing files that changed from the base of the PR and between 5e380b7 and 54cf710.

⛔ Files ignored due to path filters (1)
  • ios/CoveCore/Sources/CoveCore/generated/cove.swift is excluded by !**/generated/**
📒 Files selected for processing (7)
  • android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt
  • android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
  • ios/Cove/Views/SidebarView.swift
  • rust/src/backup/model.rs
  • rust/src/database.rs
  • rust/src/database/wallet.rs
  • rust/src/wallet/metadata.rs
✅ Files skipped from review due to trivial changes (1)
  • rust/src/backup/model.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • rust/src/database.rs

Comment thread android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt Outdated
@diyaayay diyaayay force-pushed the cove-sort-wallets-hamburger-menu branch 2 times, most recently from 4304349 to 082d8ca Compare April 21, 2026 19:12
@diyaayay diyaayay requested a review from praveenperera April 22, 2026 13:45
Copy link
Copy Markdown
Contributor

@praveenperera praveenperera left a comment

Choose a reason for hiding this comment

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

I think the Mutex is functionally correct, but I’d prefer sequencing these writes by chaining off the previous reorder Job instead. What we’re protecting here isn’t really shared mutable state, it’s the ordering of side effects: we need reorder persists to run strictly in drag order so an older drag can’t finish later and write a stale wallet order back over a newer one.

Using a pendingReorderJob?.join() style flow would make that intent more explicit, and it would also bring Android in line with iOS, which is already doing the equivalent by awaiting the previous Task before starting the next persist. Keeping both platforms on the same serialization pattern will make this code easier to reason about, easier to review, and less likely to drift into subtle platform-specific behavior the next time we touch wallet reordering.

Comment thread ios/Cove/Views/SidebarView.swift Outdated
Comment thread android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt Outdated
@diyaayay diyaayay force-pushed the cove-sort-wallets-hamburger-menu branch from 082d8ca to 1eb4756 Compare April 23, 2026 19:29
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

🧹 Nitpick comments (3)
ios/Cove/Views/SidebarView.swift (1)

192-219: Reorder task chain is never cleared.

reorderTask is reassigned each call via reorderTask = Task { await pending?.value … }, but after the final task completes it's retained indefinitely. Not a functional bug (subsequent await pending?.value returns immediately on a finished Task), but you may want to self.reorderTask = nil after the work completes so the task reference can be released, particularly since each task captures appRef and database.

Optional cleanup
-    private func persistWalletOrder(orderedIds: [WalletId]) {
-        let database = app.database
-        let appRef = app
-        let pending = reorderTask
-        reorderTask = Task {
-            await pending?.value
-
-            do {
+    private func persistWalletOrder(orderedIds: [WalletId]) {
+        let database = app.database
+        let appRef = app
+        let pending = reorderTask
+        let task = Task {
+            await pending?.value
+            do {
                 let persisted = try await Task.detached {
                     try database.wallets().reorderWallets(orderedIds: orderedIds)
                 }.value
-
                 await MainActor.run {
                     if appRef.wallets.map(\.id) == orderedIds {
                         appRef.wallets = persisted
                         HapticFeedback.success.trigger()
                     }
                 }
             } catch {
                 Log.error("Failed to reorder wallets: \(error)")
                 await MainActor.run {
                     if appRef.wallets.map(\.id) == orderedIds {
                         appRef.loadWallets()
                     }
                 }
             }
         }
+        reorderTask = task
+        Task { `@MainActor` in
+            await task.value
+            if reorderTask === task { reorderTask = nil }
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Cove/Views/SidebarView.swift` around lines 192 - 219, The
persistWalletOrder Task stored in reorderTask is never cleared, causing retained
captures of appRef and database; update persistWalletOrder so that after the
Task's work (both success and error paths) completes you set self.reorderTask =
nil (ensure this is done on the correct actor/MainActor as needed) — locate the
Task creation in persistWalletOrder (where reorderTask = Task { await
pending?.value ... }) and add logic at the end of the Task body (or in
finally-style handling) to clear reorderTask so the reference is released.
android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt (2)

232-240: Redundant DB read in the success path.

onAdoptAuthoritative already receives the authoritative List<WalletMetadata> returned by reorderWallets, but the callback then calls app.loadWallets(), which re-reads database.wallets().all() from Main. You can avoid the extra synchronous DB hit by assigning the list directly, e.g. app.wallets = authoritative (or expose a small setter on AppManager). Same idea applies to onFailureResync — if you already did app.loadWallets(), assigning walletList = app.wallets on the next line is only guaranteed to match if loadWallets ran synchronously, which is the case today but ties you to that implementation detail.

Suggested simplification
-                                            onAdoptAuthoritative = { authoritative ->
-                                                walletList = authoritative
-                                                app.loadWallets()
-                                            },
+                                            onAdoptAuthoritative = { authoritative ->
+                                                app.wallets = authoritative
+                                                walletList = authoritative
+                                            },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt` around
lines 232 - 240, The callbacks are causing redundant DB reads: in
onAdoptAuthoritative you receive the authoritative List<WalletMetadata> from
reorderWallets but then call app.loadWallets() which re-reads the DB; change the
handler to assign that list directly (e.g., set app.wallets or a new setter on
AppManager) and set walletList = authoritative instead of calling
app.loadWallets(); likewise in onFailureResync avoid the pattern
app.loadWallets(); walletList = app.wallets — either call loadWallets() alone
or, if you already have the updated list from reorderWallets/failure path,
assign it directly to app.wallets and walletList to avoid relying on synchronous
loadWallets() behavior.

95-95: pendingReorderJob doesn't need to be mutableStateOf.

Nothing in the composition reads pendingReorderJob; it's only captured by the onDragEnd closure for chaining. Wrapping it in mutableStateOf causes an unnecessary recomposition of SidebarView every time a reorder starts/completes. Using a plain remember-held holder avoids that:

Suggested change
-    var pendingReorderJob by remember { mutableStateOf<Job?>(null) }
+    val pendingReorderJobRef = remember { arrayOfNulls<Job>(1) }
@@
-                                onDragEnd = {
-                                    val orderedIds = walletList.map { it.id }
-                                    pendingReorderJob =
-                                        persistReorder(
-
-                                            previousJob = pendingReorderJob,
-
-                                        )
+                                onDragEnd = {
+                                    val orderedIds = walletList.map { it.id }
+                                    pendingReorderJobRef[0] =
+                                        persistReorder(
+
+                                            previousJob = pendingReorderJobRef[0],
+
+                                        )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt` at line
95, The variable pendingReorderJob in SidebarView is declared with
mutableStateOf causing unnecessary recompositions; change it to a plain
remember-held mutable holder (e.g., use remember { arrayOfNulls<Job?>(1) } or an
AtomicReference) and update all reads/writes to use that holder (replace uses of
pendingReorderJob with holder[0] or ref.get()/set()) so onDragEnd can still
access and chain the Job but the composition won't recompose on changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt`:
- Line 93: The shared mutable state itemHeightPx used by WalletItem leads to
incorrect threshold math (step = itemHeightPx + spacingPx) when rows differ;
change to track height per-wallet (e.g., use a mutableStateMapOf<WalletId,
Float>() keyed by the wallet's id) or store/measure height for the currently
dragged item only, update reads/writes to use that per-id height instead of
itemHeightPx, and adjust the threshold calculation (step) to reference the
per-wallet height for the dragged item's id so swaps use the correct row height.

---

Nitpick comments:
In `@android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt`:
- Around line 232-240: The callbacks are causing redundant DB reads: in
onAdoptAuthoritative you receive the authoritative List<WalletMetadata> from
reorderWallets but then call app.loadWallets() which re-reads the DB; change the
handler to assign that list directly (e.g., set app.wallets or a new setter on
AppManager) and set walletList = authoritative instead of calling
app.loadWallets(); likewise in onFailureResync avoid the pattern
app.loadWallets(); walletList = app.wallets — either call loadWallets() alone
or, if you already have the updated list from reorderWallets/failure path,
assign it directly to app.wallets and walletList to avoid relying on synchronous
loadWallets() behavior.
- Line 95: The variable pendingReorderJob in SidebarView is declared with
mutableStateOf causing unnecessary recompositions; change it to a plain
remember-held mutable holder (e.g., use remember { arrayOfNulls<Job?>(1) } or an
AtomicReference) and update all reads/writes to use that holder (replace uses of
pendingReorderJob with holder[0] or ref.get()/set()) so onDragEnd can still
access and chain the Job but the composition won't recompose on changes.

In `@ios/Cove/Views/SidebarView.swift`:
- Around line 192-219: The persistWalletOrder Task stored in reorderTask is
never cleared, causing retained captures of appRef and database; update
persistWalletOrder so that after the Task's work (both success and error paths)
completes you set self.reorderTask = nil (ensure this is done on the correct
actor/MainActor as needed) — locate the Task creation in persistWalletOrder
(where reorderTask = Task { await pending?.value ... }) and add logic at the end
of the Task body (or in finally-style handling) to clear reorderTask so the
reference is released.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 076ebc96-95a4-40c1-85aa-ceb6cd870eca

📥 Commits

Reviewing files that changed from the base of the PR and between 54cf710 and 1eb4756.

📒 Files selected for processing (2)
  • android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt
  • ios/Cove/Views/SidebarView.swift

@diyaayay
Copy link
Copy Markdown
Contributor Author

I think the Mutex is functionally correct, but I’d prefer sequencing these writes by chaining off the previous reorder Job instead. What we’re protecting here isn’t really shared mutable state, it’s the ordering of side effects: we need reorder persists to run strictly in drag order so an older drag can’t finish later and write a stale wallet order back over a newer one.

Using a pendingReorderJob?.join() style flow would make that intent more explicit, and it would also bring Android in line with iOS, which is already doing the equivalent by awaiting the previous Task before starting the next persist. Keeping both platforms on the same serialization pattern will make this code easier to reason about, easier to review, and less likely to drift into subtle platform-specific behavior the next time we touch wallet reordering.

@praveenperera Switched to pendingReorderJob?.join() chaining, structurally equivalent to iOS's await pending?.value. Also resync from the DB on failure on both platforms now, and moved the iOS success haptic inside the stillCurrent guard. Let me know if any other changes are needed.

@diyaayay diyaayay requested a review from praveenperera April 24, 2026 19:16
@praveenperera
Copy link
Copy Markdown
Contributor

thanks @diyaayay this looks good, but I need to do some manual testing on device, will do that and merge soon. Thanks for your contribution.

@praveenperera praveenperera force-pushed the cove-sort-wallets-hamburger-menu branch from 1eb4756 to 7aeafad Compare April 27, 2026 16:16
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 (2)
rust/src/database.rs (1)

141-143: ⚠️ Potential issue | 🟠 Major

Don't continue startup after migrate_positions() fails.

This migration repairs the persisted wallet-order invariant. Logging and returning Ok here can leave duplicate/broken position data in place, so the app comes up with ordering semantics partially enabled and can show the wrong order after restart. Fail init() instead of treating this as best-effort.

🐛 Proposed fix
-        if let Err(err) = wallets.migrate_positions() {
-            error!("wallet position migration failed: {err}");
-        }
+        wallets.migrate_positions()?;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@rust/src/database.rs` around lines 141 - 143, The current code swallows
errors from wallets.migrate_positions() and continues startup; instead propagate
failure so init() returns Err and prevents the app from starting with broken
position state. Replace the if-let logging branch in init() that calls
wallets.migrate_positions() so that any error is returned (e.g., use ? or map
the error into init()'s error type) rather than logging and returning Ok,
ensuring wallets.migrate_positions() failures abort initialization and bubble up
to the caller.
rust/src/database/wallet.rs (1)

239-272: ⚠️ Potential issue | 🟠 Major

Make migrate_positions atomic to avoid stale-clone overwrite.

Line 248 reads via get_all, then Line 271 writes via save_all_wallets in a separate transaction. A concurrent metadata write between those calls can still be clobbered by the migration save.

Suggested fix (single write txn per (network, mode) migration pass)
 pub fn migrate_positions(&self) -> Result<(), Error> {
     use std::collections::HashSet;
     use strum::IntoEnumIterator;

     for network in Network::iter() {
         for mode in WalletMode::iter() {
-            let wallets = self.get_all(network, mode)?;
-
-            if wallets.len() <= 1 {
-                continue;
-            }
-
-            let mut seen = HashSet::with_capacity(wallets.len());
-            let has_duplicates = wallets.iter().any(|w| !seen.insert(w.position));
-            if !has_duplicates {
-                continue;
-            }
-
-            debug!("migrating wallet positions for {network}/{mode}");
-
-            let migrated: Vec<WalletMetadata> = wallets
-                .into_iter()
-                .enumerate()
-                .map(|(i, mut w)| {
-                    w.position = i as u32;
-                    w
-                })
-                .collect();
-
-            self.save_all_wallets(network, mode, migrated)?;
+            let write_txn = self.db.begin_write()?;
+            let key = WalletKey::from((network, mode)).to_string();
+
+            let changed = {
+                let mut table = write_txn.open_table(TABLE)?;
+                let mut wallets: Vec<WalletMetadata> = table
+                    .get(key.as_str())
+                    .map_err_str(WalletTableError::ReadError)?
+                    .map(|value| value.value())
+                    .unwrap_or_default();
+
+                wallets.sort_by_key(|w| w.position);
+                if wallets.len() <= 1 {
+                    false
+                } else {
+                    let mut seen = HashSet::with_capacity(wallets.len());
+                    let has_duplicates = wallets.iter().any(|w| !seen.insert(w.position));
+                    if has_duplicates {
+                        debug!("migrating wallet positions for {network}/{mode}");
+                        for (i, w) in wallets.iter_mut().enumerate() {
+                            w.position = i as u32;
+                        }
+                        table
+                            .insert(&*key, wallets)
+                            .map_err_str(WalletTableError::SaveError)?;
+                    }
+                    has_duplicates
+                }
+            };
+
+            if changed {
+                write_txn
+                    .commit()
+                    .map_err_str(WalletTableError::SaveError)?;
+            }
         }
     }

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

In `@rust/src/database/wallet.rs` around lines 239 - 272, migrate_positions
currently reads wallets with get_all then later calls save_all_wallets which
runs a separate write txn, allowing concurrent writes to be lost; make the
migration atomic by performing the read-and-update inside a single write
transaction: for each Network::iter()/WalletMode::iter() pair open one write
txn, fetch wallets using a txn-aware read (or call a new get_all_with_txn
variant), detect duplicates and adjust positions in-place, then write them back
with a txn-aware save_all_wallets (or a new save_all_wallets_with_txn) and
commit the txn; update or add txn-accepting overloads for get_all and
save_all_wallets and use those from migrate_positions so the read and write
happen inside the same transaction.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt`:
- Around line 222-240: onDragEnd currently always calls persistReorder even when
the order didn't change; fix by capturing the pre-drag order (e.g.,
originalOrderedIds = walletList.map { it.id } in the drag start handler) and in
onDragEnd compare that saved originalOrderedIds to the current walletList.map {
it.id } and only call persistReorder (and set pendingReorderJob) when they
differ; update references to onDragEnd, persistReorder, pendingReorderJob and
walletList so unchanged drags are skipped and no write/haptic is triggered.

---

Duplicate comments:
In `@rust/src/database.rs`:
- Around line 141-143: The current code swallows errors from
wallets.migrate_positions() and continues startup; instead propagate failure so
init() returns Err and prevents the app from starting with broken position
state. Replace the if-let logging branch in init() that calls
wallets.migrate_positions() so that any error is returned (e.g., use ? or map
the error into init()'s error type) rather than logging and returning Ok,
ensuring wallets.migrate_positions() failures abort initialization and bubble up
to the caller.

In `@rust/src/database/wallet.rs`:
- Around line 239-272: migrate_positions currently reads wallets with get_all
then later calls save_all_wallets which runs a separate write txn, allowing
concurrent writes to be lost; make the migration atomic by performing the
read-and-update inside a single write transaction: for each
Network::iter()/WalletMode::iter() pair open one write txn, fetch wallets using
a txn-aware read (or call a new get_all_with_txn variant), detect duplicates and
adjust positions in-place, then write them back with a txn-aware
save_all_wallets (or a new save_all_wallets_with_txn) and commit the txn; update
or add txn-accepting overloads for get_all and save_all_wallets and use those
from migrate_positions so the read and write happen inside the same transaction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9af96881-bb00-4fea-964b-a93f872073b2

📥 Commits

Reviewing files that changed from the base of the PR and between 1eb4756 and 7aeafad.

⛔ Files ignored due to path filters (1)
  • ios/CoveCore/Sources/CoveCore/generated/cove.swift is excluded by !**/generated/**
📒 Files selected for processing (7)
  • android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt
  • android/app/src/main/java/org/bitcoinppl/cove_core/cove.kt
  • ios/Cove/Views/SidebarView.swift
  • rust/src/backup/model.rs
  • rust/src/database.rs
  • rust/src/database/wallet.rs
  • rust/src/wallet/metadata.rs
✅ Files skipped from review due to trivial changes (2)
  • rust/src/backup/model.rs
  • rust/src/wallet/metadata.rs

Comment on lines +222 to +240
onDragEnd = {
val orderedIds = walletList.map { it.id }
pendingReorderJob =
persistReorder(
scope = scope,
app = app,
previousJob = pendingReorderJob,
orderedIds = orderedIds,
haptics = haptics,
currentOptimisticIds = { walletList.map { it.id } },
onAdoptAuthoritative = { authoritative ->
walletList = authoritative
app.loadWallets()
},
onFailureResync = {
app.loadWallets()
walletList = app.wallets
},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Skip persistence when the drag didn’t actually reorder anything.

onDragEnd always queues reorderWallets(). A long-press/release that never crosses a swap threshold still does a write transaction and later emits the success haptic, even though the order is unchanged.

💡 Proposed fix
                                 onDragEnd = {
                                     val orderedIds = walletList.map { it.id }
-                                    pendingReorderJob =
-                                        persistReorder(
-                                            scope = scope,
-                                            app = app,
-                                            previousJob = pendingReorderJob,
-                                            orderedIds = orderedIds,
-                                            haptics = haptics,
-                                            currentOptimisticIds = { walletList.map { it.id } },
-                                            onAdoptAuthoritative = { authoritative ->
-                                                walletList = authoritative
-                                                app.loadWallets()
-                                            },
-                                            onFailureResync = {
-                                                app.loadWallets()
-                                                walletList = app.wallets
-                                            },
-                                        )
+                                    if (orderedIds != preDragSnapshot.map { it.id }) {
+                                        pendingReorderJob =
+                                            persistReorder(
+                                                scope = scope,
+                                                app = app,
+                                                previousJob = pendingReorderJob,
+                                                orderedIds = orderedIds,
+                                                haptics = haptics,
+                                                currentOptimisticIds = { walletList.map { it.id } },
+                                                onAdoptAuthoritative = { authoritative ->
+                                                    walletList = authoritative
+                                                    app.loadWallets()
+                                                },
+                                                onFailureResync = {
+                                                    app.loadWallets()
+                                                    walletList = app.wallets
+                                                },
+                                            )
+                                    }
                                     draggedWalletId = null
                                     draggedOffsetY = 0f
                                 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@android/app/src/main/java/org/bitcoinppl/cove/sidebar/SidebarView.kt` around
lines 222 - 240, onDragEnd currently always calls persistReorder even when the
order didn't change; fix by capturing the pre-drag order (e.g.,
originalOrderedIds = walletList.map { it.id } in the drag start handler) and in
onDragEnd compare that saved originalOrderedIds to the current walletList.map {
it.id } and only call persistReorder (and set pendingReorderJob) when they
differ; update references to onDragEnd, persistReorder, pendingReorderJob and
walletList so unchanged drags are skipped and no write/haptic is triggered.

@praveenperera praveenperera force-pushed the cove-sort-wallets-hamburger-menu branch from 7aeafad to 81c0c42 Compare April 27, 2026 18:33
@praveenperera praveenperera force-pushed the cove-sort-wallets-hamburger-menu branch from 81c0c42 to a918d7a Compare April 27, 2026 19:12
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.

Add ability to sort wallets in hamburger menu

3 participants