Skip to content

Conversation

@yewreeka
Copy link
Contributor

@yewreeka yewreeka commented Nov 11, 2025

Replace destructive confirmation dialog with a non-dismissible self-sizing sheet and hold-to-delete flow for deleting all app data in AppSettingsView

Introduce AppSettingsViewModel to manage delete-all progress and errors, present DeleteAllDataView via a sheet with a 3-second hold-to-delete button, route session deletion through SessionManagerProtocol.deleteAllInboxesWithProgress(), and add waitForDeletionComplete() across inbox and messaging layers.

📍Where to Start

Start with the delete-all flow entry in AppSettingsView and its view model: AppSettingsView.swift and AppSettingsViewModel.swift. Then review the progress stream in SessionManager.swift.


📊 Macroscope summarized 948435a. 17 files reviewed, 11 issues evaluated, 9 issues filtered, 1 comment posted

🗂️ Filtered Issues

Convos/App Settings/AppSettingsView.swift — 0 comments posted, 1 evaluated, 1 filtered
  • [line 166](https://github.com/ephemeraHQ/convos-ios/blob/948435a651407613205b6d0fea6f550b968ceb1f/Convos/App Settings/AppSettingsView.swift#L166): The refactored code removes the quicknameViewModel.delete() call that was present in the old confirmation dialog. The old code explicitly deleted the quickname when the user confirmed deletion, but the new DeleteAllDataView only calls viewModel.deleteAllData(onComplete:) without any reference to quicknameViewModel. Unless AppSettingsViewModel.deleteAllData internally handles quickname deletion, the quickname data will no longer be deleted when the user chooses to delete all app data, despite the UI stating "This will permanently delete all conversations on this device, as well as your Quickname." [ Low confidence ]
Convos/App Settings/AppSettingsViewModel.swift — 1 comment posted, 2 evaluated, 1 filtered
  • [line 37](https://github.com/ephemeraHQ/convos-ios/blob/948435a651407613205b6d0fea6f550b968ceb1f/Convos/App Settings/AppSettingsViewModel.swift#L37): isDeleting is never reset to false after successful deletion. The variable is set to true at the start of deleteAllData, but only the catch block resets it to false. On success, onComplete() is called but isDeleting remains true. This will cause the guard !isDeleting else { return } check to silently reject all subsequent deletion attempts. [ Already posted ]
Convos/Conversations List/ConversationsView.swift — 0 comments posted, 2 evaluated, 1 filtered
  • [line 165](https://github.com/ephemeraHQ/convos-ios/blob/948435a651407613205b6d0fea6f550b968ceb1f/Convos/Conversations List/ConversationsView.swift#L165): The deletion flow no longer calls quicknameViewModel.delete(). The old confirmationDialog implementation explicitly called quicknameViewModel.delete() before onDeleteAllData(), but the new selfSizingSheet with DeleteAllDataView only calls dismiss() and onDeleteAllData() in onComplete. Unless DeleteAllDataView internally handles quickname deletion, user quickname data may not be properly deleted when the user deletes all app data. [ Already posted ]
Convos/Conversations List/ConversationsViewModel.swift — 0 comments posted, 2 evaluated, 2 filtered
Convos/Shared Views/HoldToConfirmButton.swift — 0 comments posted, 1 evaluated, 1 filtered
  • [line 69](https://github.com/ephemeraHQ/convos-ios/blob/948435a651407613205b6d0fea6f550b968ceb1f/Convos/Shared Views/HoldToConfirmButton.swift#L69): If config.duration is set to 0 or a negative value, line 69 performs a division by zero or produces negative/NaN progress values. This would cause scaleEffect(currentProgress) at line 93 to behave incorrectly (infinite scale or negative scale). Additionally, onLongPressGesture(minimumDuration:) at line 103 may exhibit undefined behavior with non-positive duration values. [ Low confidence ]
ConvosCore/Sources/ConvosCore/Inboxes/InboxStateMachine.swift — 0 comments posted, 1 evaluated, 1 filtered
  • line 298: The method waitForDeletionComplete() may hang indefinitely if called when a deletion has not been initiated. If the state machine is in a state like .ready and no stopAndDelete() action is queued, the method will wait forever for a state transition to .idle, .stopping, or .error that may never occur. Consider adding a timeout or checking that a deletion is in progress before waiting. [ Already posted ]
ConvosCore/Sources/ConvosCore/Inboxes/InboxStateManager.swift — 0 comments posted, 2 evaluated, 2 filtered
  • line 134: Race condition in waitForDeletionComplete(): The method iterates over stateSequence which immediately yields the current state when the continuation is added. If stopAndDelete() enqueues the delete action but the action hasn't been processed yet (still in .idle state), waitForDeletionComplete() will see .idle as the first yielded state and return immediately, incorrectly thinking deletion is complete. This happens because enqueueAction(.delete) is fire-and-forget and the actual state transition occurs asynchronously in a separate Task. [ Already posted ]
  • line 135: Silent failure in waitForDeletionComplete() when stateMachine is nil: Unlike the delete() method which throws InboxStateError.inboxNotReady when stateMachine is nil, this method silently returns. Callers have no way to distinguish between successful deletion completion and the state machine being unavailable, which could lead to incorrect assumptions about deletion state. [ Low confidence ]

@yewreeka yewreeka marked this pull request as ready for review November 11, 2025 06:20
Copy link
Contributor Author

yewreeka commented Nov 11, 2025


How to use the Graphite Merge Queue

Add the label merge-queue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@yewreeka yewreeka force-pushed the improve-delete-all branch 2 times, most recently from ed2051a to 276766e Compare November 11, 2025 06:31
@claude
Copy link

claude bot commented Nov 11, 2025

Claude finished @yewreeka's task —— View job


Code Review: Improve delete all flow

I've completed a thorough review of PR #237. Overall, this is a well-structured implementation that adds a nice UX improvement for the delete-all flow. However, there are several critical bugs and improvements needed before merge.


🔴 Critical Issues

1. Missing isDeleting = false after successful deletion (AppSettingsViewModel.swift:37)

The isDeleting flag is never reset after successful deletion, which will prevent any future deletion attempts and leave the UI in a loading state indefinitely.

// Lines 33-37
for try await progress in session.deleteAllInboxesWithProgress() {
    deletionProgress = progress
}

// Deletion complete
onComplete()  // ⚠️ Missing: isDeleting = false

Fix: Add isDeleting = false before calling onComplete().

Reference: Macroscope correctly flagged this at AppSettingsViewModel.swift:37.


2. Race condition: No reentrancy guard (AppSettingsViewModel.swift:25)

Multiple concurrent calls to deleteAllData can spawn overlapping deletion tasks. The guard at line 25 doesn't prevent this because it checks isDeleting before setting it, creating a window for race conditions.

func deleteAllData(onComplete: @escaping () -> Void) {
    guard !isDeleting else { return }  // ✅ Good
    isDeleting = true
    // ... rest of deletion
}

While the guard exists, the concern is valid if this method could be called from different contexts rapidly.

Fix: The existing guard is actually sufficient for @MainActor isolation, but ensure UI prevents double-taps.

Reference: Macroscope flagged this at AppSettingsViewModel.swift:28.


3. Non-atomic deletion flow with premature side effects (AppSettingsViewModel.swift:30)

QuicknameSettingsViewModel.shared.delete() is called before the async deletion completes, creating an inconsistent state if deletion fails. The quickname is deleted but inboxes remain.

func deleteAllData(onComplete: @escaping () -> Void) {
    guard !isDeleting else { return }
    isDeleting = true
    deletionError = nil
    deletionProgress = nil

    QuicknameSettingsViewModel.shared.delete()  // ⚠️ Called too early!

    Task {
        do {
            for try await progress in session.deleteAllInboxesWithProgress() {
                deletionProgress = progress
            }
            onComplete()
        } catch {
            deletionError = error
            isDeleting = false
        }
    }
}

Fix: Move QuicknameSettingsViewModel.shared.delete() inside the do block after the async stream completes.

Reference: Macroscope flagged this at AppSettingsViewModel.swift:42.


4. Missing cancellation handler in progress stream (SessionManager.swift:253)

The background Task in deleteAllInboxesWithProgress() is not cancelled when the stream consumer terminates early. This could leave deletion operations running in the background.

AsyncThrowingStream { continuation in
    let task = Task {
        // deletion work
    }
    // ⚠️ Missing: continuation.onTermination = { _ in task.cancel() }
}

Fix: Add continuation.onTermination = { _ in task.cancel() } after creating the task.

Reference: Macroscope flagged this review comment on SessionManager.swift.


5. waitForDeletionComplete() may return immediately (InboxStateMachine.swift:298-312)

The implementation returns immediately if the state is already .idle, .stopping, or .error. But InboxStateManager.delete() calls this right after stopAndDelete(), potentially before the deletion actually starts.

public func waitForDeletionComplete() async {
    for await state in stateSequence {
        switch state {
        case .idle, .stopping:
            return  // ⚠️ May return before deletion even starts
        case .error:
            return
        default:
            continue
        }
    }
}

Fix: Wait for .deleting state first, then wait for terminal state.

Reference: Macroscope flagged this at InboxStateManager.swift:126.


🟡 Medium Priority Issues

6. ShatteringText doesn't regenerate on text change (ShatteringText.swift:61)

The .task(id: text) modifier regenerates values when text changes, which is correct. However, the arrays (letterOffsets, letterRotations, letterScales) are not updated immediately when text length changes, which could cause index-out-of-bounds issues.

.task(id: text) { generateRandomValues() }  // ✅ Already present

The current implementation is actually fine - .task(id:) will call generateRandomValues() when text changes. Macroscope's suggestion to also add .onChange(of: text) is redundant.

Status: No fix needed - current implementation is correct.

Reference: Macroscope comment on ShatteringText.swift.


🟢 Code Quality & Best Practices

Strengths:

  1. Good separation of concerns: AppSettingsViewModel properly abstracts business logic from UI
  2. Modern Swift concurrency: Excellent use of AsyncThrowingStream for progress reporting
  3. Proper @mainactor isolation: UI state updates are correctly isolated
  4. Reusable components: HoldToConfirmButton and ShatteringText are well-extracted
  5. Comprehensive deletion: SessionManager.deleteAllInboxesWithProgress() properly cleans up device registration, services, and database
  6. Progress tracking: Users get clear feedback during the deletion process

📝 Minor Improvements:

  1. Add documentation: AppSettingsViewModel.deleteAllData should document the deletion flow
  2. Consider adding analytics: Track deletion events for user behavior analysis
  3. Error messages: DeleteAllDataView shows raw error.localizedDescription - consider user-friendly messages

🔒 Security Concerns

No security issues found. The deletion flow properly:

  • Cleans up device registration
  • Removes all local data (keychain, database, XMTP files)
  • Unsubscribes from push topics
  • Unregisters installations from backend

⚡ Performance Considerations

Good performance design:

  1. Parallel deletion with withTaskGroup for multiple services
  2. Deferred device registration cleanup ensures it happens even on error
  3. Cancellation-aware tasks with proper Task.checkCancellation()

Minor concern: deleteDatabaseFiles() uses synchronous FileManager operations on the main queue. Consider moving to background queue if this becomes slow.


🧪 Test Coverage

⚠️ No tests found for new functionality. Recommend adding:

  1. Unit tests for AppSettingsViewModel.deleteAllData()
  2. Integration tests for SessionManager.deleteAllInboxesWithProgress()
  3. UI tests for DeleteAllDataView hold-to-delete interaction
  4. Tests for error scenarios (network failure, database errors)

📋 Summary

Blockers before merge:

  1. Fix missing isDeleting = false after successful deletion ✋
  2. Move QuicknameSettingsViewModel.shared.delete() to success path ✋
  3. Add cancellation handler to progress stream ✋
  4. Fix waitForDeletionComplete() race condition ✋

Nice to have:

  • Add tests for deletion flow
  • Improve error messaging for users
  • Add documentation

The core architecture is solid, but the state management issues need to be addressed to prevent bugs in production. The UX improvements (hold-to-delete, progress tracking, shattering animation) are excellent additions.



QuicknameSettingsViewModel.shared.delete()

Task {
Copy link

Choose a reason for hiding this comment

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

The Task in deleteAllData isn’t tracked. If this view model deallocates during deletion, onComplete() can still fire and drive UI on a dead screen. Consider storing the task and cancelling it in deinit or when appropriate to avoid running completion after teardown.

🚀 Reply to ask Macroscope to explain or update this suggestion.

👍 Helpful? React to give us feedback.

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.

2 participants