Keep mirrored outgoing media when clearing another conversation

Clearing conversation A removed outgoing media IDs from every direct
conversation and unlinked the payload unconditionally, so a mirrored
outgoing bubble in conversation B (identity-alias handoff) lost both
its row and its file. Outgoing removal and file cleanup now mirror the
incoming alias protection: only IDs with no surviving copy in another
conversation are removed everywhere and unlinked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jack
2026-07-26 13:37:07 +02:00
co-authored by Claude Opus 4.8
parent 6e1da519ca
commit 867711506d
2 changed files with 67 additions and 4 deletions
+52
View File
@@ -1821,6 +1821,58 @@ struct ChatViewModelPrivateMediaDeletionTests {
)
}
@Test @MainActor
func clearPrivateChatKeepsOutgoingMediaReferencedByAnotherConversation()
throws {
let (viewModel, transport) = makeTestableViewModel()
let firstPeerID = PeerID(str: String(repeating: "4", count: 64))
let aliasPeerID = PeerID(str: String(repeating: "5", count: 64))
let outgoingDirectory = try FileManager.default.url(
for: .applicationSupportDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
.appendingPathComponent("files/images/outgoing", isDirectory: true)
try FileManager.default.createDirectory(
at: outgoingDirectory,
withIntermediateDirectories: true
)
let fileURL = outgoingDirectory.appendingPathComponent(
"outgoing-mirrored-\(UUID().uuidString).jpg"
)
try Data("outgoing-image".utf8).write(to: fileURL, options: .atomic)
defer { try? FileManager.default.removeItem(at: fileURL) }
let message = privateMediaMessage(
id: UUID().uuidString,
sender: viewModel.nickname,
senderPeerID: transport.myPeerID,
recipient: "Peer",
filename: fileURL.lastPathComponent
)
viewModel.seedPrivateChat([message], for: firstPeerID)
viewModel.seedPrivateChat([message], for: aliasPeerID)
viewModel.clearPrivateChat(firstPeerID)
// The mirrored conversation keeps its bubble and the payload file:
// clearing conversation A must not reach across an identity-alias
// handoff into conversation B.
#expect(transport.deletedPrivateMediaMessageIDBatches.isEmpty)
#expect(viewModel.privateChats[firstPeerID]?.isEmpty == true)
#expect(
viewModel.privateChats[aliasPeerID]?.map(\.id) == [message.id]
)
#expect(FileManager.default.fileExists(atPath: fileURL.path))
// Clearing the last conversation that references the outgoing
// payload removes both the bubble and the file.
viewModel.clearPrivateChat(aliasPeerID)
#expect((viewModel.privateChats[aliasPeerID] ?? []).isEmpty)
#expect(!FileManager.default.fileExists(atPath: fileURL.path))
}
@Test @MainActor
func clearPrivateChatLeavesAmbiguousLegacyFileForQuotaCleanup()
throws {