Skip to content

feat(ios): offer to trash the original when removing an offline copy - #229

Merged
altaywtf merged 1 commit into
nextfrom
feat/offline-remove-original-1
Sep 13, 2026
Merged

feat(ios): offer to trash the original when removing an offline copy#229
altaywtf merged 1 commit into
nextfrom
feat/offline-remove-original-1

Conversation

@altaywtf

Copy link
Copy Markdown
Member

Problem

Removing an offline download only cleared the local copy; there was no way to also let go of the put.io original. Any such action must stay explicit, follow the account's Trash setting, and never report success the server did not confirm.

Refs #106. First of three stacked PRs; the next two add durable retries across relaunch and Trash-setting confirmation.

Solution

  • Removing a download (swipe, context menu, or multi-select) confirms through a dialog: Remove download (default, local only), Remove download and move original to Trash (destructive; worded …and delete original when Trash is disabled, with no recovery claim), and Cancel. The title names the file or count; the message says other devices signed in to the account are affected. Both choices carry distinct labels and identifiers for VoiceOver.
  • The dialog copy is built from the mode captured with the presented targets, so a preference refresh while it is open cannot make the text disagree with the action.
  • The original choice is disabled while account preferences are stale or saving, the gate the Files browser applies, because PutioRuntime.deleteFile refuses those requests.
  • PutioOfflineQueue.removeDeletingOriginals removes the local copies first, then asks put.io for each original. A remote failure never resurrects the local copy: the queue keeps a per-file failure report with Try again; a notFound answer counts as done. The delete request refuses to run under another account after a sign-out.
  • Seeded API: the first delete of the root video fails with a 503 so the journey proves the honest failure report and retry. Harness contract gains runtime-downloads-remove.png.

Proof

  • mise run verify, mise run harness -- test --platform ios, and mise run harness -- journey --platform ios --scenario files-browser passed locally on this commit.
  • OfflineDownloadsTests cover local-before-remote ordering, plain removal never touching the original, partial remote failure with retry, reports acting only on the failures they showed, merged failures across concurrent requests, error-reason mapping, and Trash-aware copy.

Removing a download now confirms through a dialog whose default keeps the
put.io original; a separately worded destructive choice asks put.io for it
after the local copy is gone. Remote failures are reported per file with a
retry and never resurrect the local copy. The dialog copy follows the mode
captured with the targets, so a preference refresh cannot make the text
disagree with the action.

Refs #106
Copilot AI lite review requested due to automatic review settings September 13, 2026 04:14
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 13, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-13T04:23:06.573321Z c168fad PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c168fad12d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Apps/iOS/Sources/OfflineDownloads.swift
Comment thread Apps/iOS/Sources/PutioApp.swift
Comment thread Apps/Shared/Sources/HarnessSeededAPI.swift
Comment thread Apps/iOS/Sources/OfflineDownloads.swift
Comment thread Apps/iOS/Sources/OfflineRemoval.swift

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Pull request overview

Adds an explicit “remove download vs remove download + delete/move original” flow for offline items, including retryable remote-delete outcomes and updated harness/journey artifacts.

Changes:

  • Introduces offline-removal models/copy and queue support for “remove locally, then delete original” with durable failure reporting + retry.
  • Updates the iOS downloads UI to present a confirmation dialog with distinct accessible actions and a follow-up failure alert.
  • Extends harness/journey contracts + tests to cover the new dialog, screenshot, and seeded transient delete failure.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
docs/HARNESS.md Updates journey description and required screenshots to include the new removal dialog capture.
Tools/PutioHarness/Tests/PutioHarnessKitTests/ModelsTests.swift Updates expected downloads attachment list to include runtime-downloads-remove.
Tools/PutioHarness/Sources/PutioHarnessKit/Models.swift Extends BrowserJourneyContract attachment names with the new screenshot.
Tests/iOSUITests/Sources/DownloadsJourneyTests.swift Updates UI journey to validate new confirmation dialog, failure report, and retry behavior.
Tests/iOS/Sources/OfflineDownloadsTests.swift Adds extensive unit coverage for local-before-remote deletion, failure merging, retry handling, and copy.
Apps/iOS/Sources/PutioApp.swift Wires new downloads view inputs and queue deleteOriginal runtime hook.
Apps/iOS/Sources/OfflineRemoval.swift Adds new removal target/outcome types and user-facing copy helpers.
Apps/iOS/Sources/OfflineDownloadsView.swift Replaces alert with confirmation dialog and adds remote-failure retry alert + accessibility identifiers.
Apps/iOS/Sources/OfflineDownloads.swift Adds queue APIs for deleting originals and tracking merged remote failures.
Apps/Shared/Sources/HarnessSeededAPI.swift Seeds a transient first-delete failure for the root video to prove retry UX.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +612 to +633
func deleteOriginals(_ targets: [PutioOfflineRemovalTarget]) async
-> PutioOfflineOriginalOutcome
{
var outcome = PutioOfflineOriginalOutcome()
for target in targets {
do {
try await deleteOriginal(target.id)
outcome.deleted.append(target)
} catch PutioRuntimeError.notFound {
// Already gone, possibly from a delete whose response was lost.
outcome.deleted.append(target)
} catch {
outcome.failures.append(.init(target: target, reason: .init(error)))
}
}
var merged = originalFailure ?? PutioOfflineOriginalOutcome()
let touched = Set(targets.map(\.id))
merged.failures.removeAll { touched.contains($0.target.id) }
merged.failures.append(contentsOf: outcome.failures)
originalFailure = merged.failures.isEmpty ? nil : merged
return outcome
}
Comment on lines +101 to +109
let removeLocal = app.buttons["downloads.remove-local"].firstMatch
XCTAssertTrue(removeLocal.waitForExistence(timeout: 5))
XCTAssertTrue(app.staticTexts["Remove “Root Movie.mkv”?"].exists, "the title names the file")
XCTAssertTrue(
app.staticTexts.element(
matching: NSPredicate(format: "label CONTAINS %@", "every device signed in to your account")
).exists, "the confirmation does not explain that other devices are affected")
let removeOriginal = app.buttons["downloads.remove-original"].firstMatch
XCTAssertTrue(removeOriginal.exists)
Comment on lines +118 to +119
let failure = app.alerts["Could not move original to Trash"]
XCTAssertTrue(failure.waitForExistence(timeout: 10))
app.buttons["downloads.remove-original-retry"].firstMatch.tap()
XCTAssertTrue(failure.waitForNonExistence(timeout: 10))
XCTAssertTrue(app.staticTexts["No downloads"].waitForExistence(timeout: 5))
XCTAssertFalse(app.alerts.firstMatch.exists, "retry must not report a second failure")
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