mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 17:45:21 +00:00
Surface refused media deletions in the affected chat
A refused delete/clear (journal write failure, reservation conflict, path collision, capacity) only logged SecureLogger.error, so a wedged /clear looked like success. Refusals now post a localized system message (content.system.media_delete_refused, 30 locales) into the affected private chat via the existing system-message mechanism, wired at both the per-bubble delete and /clear refusal sites. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,42 @@
|
||||
{
|
||||
"sourceLanguage" : "en",
|
||||
"strings" : {
|
||||
"content.system.media_delete_refused" : {
|
||||
"comment" : "System message shown in the affected chat when an explicit media delete or /clear was refused and bubbles/files were kept",
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"ar" : { "stringUnit" : { "state" : "needs_review", "value" : "تعذّر حذف بعض الوسائط. جرّب حذف الوسائط الأقدم أولاً." } },
|
||||
"bn" : { "stringUnit" : { "state" : "needs_review", "value" : "কিছু মিডিয়া মুছে ফেলা যায়নি। আগে পুরোনো মিডিয়া মুছে ফেলার চেষ্টা করুন।" } },
|
||||
"de" : { "stringUnit" : { "state" : "needs_review", "value" : "einige medien konnten nicht gelöscht werden. versuche zuerst, ältere medien zu löschen." } },
|
||||
"en" : { "stringUnit" : { "state" : "translated", "value" : "some media could not be deleted. try deleting older media first." } },
|
||||
"es" : { "stringUnit" : { "state" : "needs_review", "value" : "no se pudieron eliminar algunos archivos multimedia. prueba a eliminar primero los más antiguos." } },
|
||||
"fa" : { "stringUnit" : { "state" : "needs_review", "value" : "برخی رسانهها حذف نشدند. ابتدا رسانههای قدیمیتر را حذف کنید." } },
|
||||
"fil" : { "stringUnit" : { "state" : "needs_review", "value" : "hindi ma-delete ang ilang media. subukang i-delete muna ang mas lumang media." } },
|
||||
"fr" : { "stringUnit" : { "state" : "needs_review", "value" : "impossible de supprimer certains médias. essaie d'abord de supprimer les médias plus anciens." } },
|
||||
"he" : { "stringUnit" : { "state" : "needs_review", "value" : "לא ניתן למחוק חלק מהמדיה. נסה קודם למחוק מדיה ישנה יותר." } },
|
||||
"hi" : { "stringUnit" : { "state" : "needs_review", "value" : "कुछ मीडिया हटाई नहीं जा सकी। पहले पुरानी मीडिया हटाने का प्रयास करें।" } },
|
||||
"id" : { "stringUnit" : { "state" : "needs_review", "value" : "sebagian media tidak dapat dihapus. coba hapus media yang lebih lama dulu." } },
|
||||
"it" : { "stringUnit" : { "state" : "needs_review", "value" : "impossibile eliminare alcuni contenuti multimediali. prova prima a eliminare quelli più vecchi." } },
|
||||
"ja" : { "stringUnit" : { "state" : "needs_review", "value" : "一部のメディアを削除できませんでした。先に古いメディアを削除してみてください。" } },
|
||||
"ko" : { "stringUnit" : { "state" : "needs_review", "value" : "일부 미디어를 삭제하지 못했습니다. 먼저 오래된 미디어를 삭제해 보세요." } },
|
||||
"ms" : { "stringUnit" : { "state" : "needs_review", "value" : "sesetengah media tidak dapat dipadamkan. cuba padamkan media yang lebih lama dahulu." } },
|
||||
"ne" : { "stringUnit" : { "state" : "needs_review", "value" : "केही मिडिया मेटाउन सकिएन। पहिले पुराना मिडिया मेटाउने प्रयास गर।" } },
|
||||
"nl" : { "stringUnit" : { "state" : "needs_review", "value" : "sommige media konden niet worden verwijderd. probeer eerst oudere media te verwijderen." } },
|
||||
"pl" : { "stringUnit" : { "state" : "needs_review", "value" : "nie udało się usunąć części multimediów. spróbuj najpierw usunąć starsze multimedia." } },
|
||||
"pt" : { "stringUnit" : { "state" : "needs_review", "value" : "não foi possível eliminar alguns ficheiros multimédia. tenta eliminar primeiro os mais antigos." } },
|
||||
"pt-BR" : { "stringUnit" : { "state" : "needs_review", "value" : "não foi possível excluir algumas mídias. tente excluir primeiro as mídias mais antigas." } },
|
||||
"ru" : { "stringUnit" : { "state" : "needs_review", "value" : "не удалось удалить часть медиафайлов. попробуй сначала удалить более старые." } },
|
||||
"sv" : { "stringUnit" : { "state" : "needs_review", "value" : "vissa medier kunde inte raderas. prova att radera äldre medier först." } },
|
||||
"ta" : { "stringUnit" : { "state" : "needs_review", "value" : "சில ஊடகங்களை நீக்க முடியவில்லை. முதலில் பழைய ஊடகங்களை நீக்க முயற்சிக்கவும்." } },
|
||||
"th" : { "stringUnit" : { "state" : "needs_review", "value" : "ไม่สามารถลบสื่อบางรายการได้ ลองลบสื่อที่เก่ากว่าก่อน" } },
|
||||
"tr" : { "stringUnit" : { "state" : "needs_review", "value" : "bazı medya silinemedi. önce daha eski medyayı silmeyi dene." } },
|
||||
"uk" : { "stringUnit" : { "state" : "needs_review", "value" : "не вдалося видалити частину медіафайлів. спробуй спочатку видалити старіші." } },
|
||||
"ur" : { "stringUnit" : { "state" : "needs_review", "value" : "کچھ میڈیا حذف نہیں ہو سکا۔ پہلے پرانا میڈیا حذف کرنے کی کوشش کریں۔" } },
|
||||
"vi" : { "stringUnit" : { "state" : "needs_review", "value" : "không thể xóa một số phương tiện. hãy thử xóa phương tiện cũ hơn trước." } },
|
||||
"zh-Hans" : { "stringUnit" : { "state" : "needs_review", "value" : "部分媒体无法删除。请先尝试删除较早的媒体。" } },
|
||||
"zh-Hant" : { "stringUnit" : { "state" : "needs_review", "value" : "部分媒體無法刪除。請先嘗試刪除較舊的媒體。" } }
|
||||
}
|
||||
},
|
||||
"notification.action.wave" : {
|
||||
"comment" : "Title of the notification action button that sends a friendly wave back to a nearby person",
|
||||
"extractionState" : "manual",
|
||||
|
||||
@@ -91,6 +91,9 @@ protocol ChatMediaTransferContext: AnyObject {
|
||||
func removeUntombstonedMediaMessage(withID messageID: String)
|
||||
func removeOutgoingMediaMessage(withID messageID: String)
|
||||
func addSystemMessage(_ content: String)
|
||||
/// Surfaces a refused explicit media deletion in the affected chat so a
|
||||
/// wedged delete never looks like success.
|
||||
func notifyMediaDeletionRefused(messageID: String)
|
||||
/// Signals that message state changed so observers refresh (e.g. `objectWillChange.send()`).
|
||||
func notifyUIChanged()
|
||||
|
||||
@@ -350,6 +353,28 @@ extension ChatViewModel: ChatMediaTransferContext {
|
||||
}
|
||||
}
|
||||
|
||||
func notifyMediaDeletionRefused(messageID: String) {
|
||||
let owningPeerID = privateChats.first { _, messages in
|
||||
messages.contains { $0.id == messageID }
|
||||
}?.key
|
||||
notifyPrivateMediaDeletionRefused(peerID: owningPeerID)
|
||||
}
|
||||
|
||||
/// A refused deletion/clear previously surfaced only in SecureLogger, so
|
||||
/// a wedged /clear looked like success. Tell the affected chat that its
|
||||
/// bubbles and payloads were intentionally kept.
|
||||
func notifyPrivateMediaDeletionRefused(peerID: PeerID?) {
|
||||
let copy = String(
|
||||
localized: "content.system.media_delete_refused",
|
||||
comment: "System message when an explicit media delete or /clear was refused and bubbles/files were kept"
|
||||
)
|
||||
if let peerID = peerID ?? selectedPrivateChatPeer {
|
||||
addLocalPrivateSystemMessage(copy, to: peerID)
|
||||
} else {
|
||||
addSystemMessage(copy)
|
||||
}
|
||||
}
|
||||
|
||||
private func isIncomingPrivateMessage(
|
||||
_ message: BitchatMessage
|
||||
) -> Bool {
|
||||
@@ -1176,6 +1201,9 @@ final class ChatMediaTransferCoordinator {
|
||||
"Refusing to delete private media without a durable tombstone id=\(messageID.prefix(12))…",
|
||||
category: .session
|
||||
)
|
||||
self.context.notifyMediaDeletionRefused(
|
||||
messageID: messageID
|
||||
)
|
||||
return
|
||||
}
|
||||
self.finishMediaDeletion(
|
||||
|
||||
@@ -837,6 +837,7 @@ final class ChatViewModel: ObservableObject, BitchatDelegate, SynchronousMessage
|
||||
"Refusing to clear private chat without durable media tombstones peer=\(peerID.id.prefix(8))…",
|
||||
category: .session
|
||||
)
|
||||
notifyPrivateMediaDeletionRefused(peerID: peerID)
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@ private final class MockChatMediaTransferContext: ChatMediaTransferContext {
|
||||
}
|
||||
|
||||
func addSystemMessage(_ content: String) { systemMessages.append(content) }
|
||||
private(set) var mediaDeletionRefusals: [String] = []
|
||||
func notifyMediaDeletionRefused(messageID: String) {
|
||||
mediaDeletionRefusals.append(messageID)
|
||||
}
|
||||
func notifyUIChanged() { notifyUIChangedCount += 1 }
|
||||
|
||||
// Delivery status & dedup
|
||||
@@ -643,6 +647,8 @@ struct ChatMediaTransferCoordinatorContextTests {
|
||||
#expect(context.removedMessages.isEmpty)
|
||||
#expect(context.cancelledTransfers == ["failed-delete"])
|
||||
#expect(coordinator.messageIDToTransferId[messageID] == nil)
|
||||
// The refusal must be visible in the affected chat, not just logged.
|
||||
#expect(context.mediaDeletionRefusals == [messageID])
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
|
||||
@@ -1272,9 +1272,13 @@ struct ChatViewModelPrivateMediaDeletionTests {
|
||||
"images/incoming/\(filename)"
|
||||
]]
|
||||
)
|
||||
let messages = viewModel.privateChats[peerID] ?? []
|
||||
#expect(messages.prefix(2).map(\.id) == [stableID, legacyID])
|
||||
// The refusal is surfaced in the affected chat, not just logged.
|
||||
#expect(messages.last?.sender == "system")
|
||||
#expect(
|
||||
viewModel.privateChats[peerID]?.map(\.id)
|
||||
== [stableID, legacyID]
|
||||
messages.last?.content
|
||||
== String(localized: "content.system.media_delete_refused")
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1374,10 +1378,17 @@ struct ChatViewModelPrivateMediaDeletionTests {
|
||||
#expect(
|
||||
transport.deletedPrivateMediaMessageIDBatches == [[incomingID]]
|
||||
)
|
||||
let messages = viewModel.privateChats[peerID] ?? []
|
||||
#expect(
|
||||
viewModel.privateChats[peerID]?.map(\.id)
|
||||
messages.prefix(3).map(\.id)
|
||||
== [incomingID, outgoingID, "ordinary-message"]
|
||||
)
|
||||
// The refused /clear is surfaced in the affected chat.
|
||||
#expect(messages.last?.sender == "system")
|
||||
#expect(
|
||||
messages.last?.content
|
||||
== String(localized: "content.system.media_delete_refused")
|
||||
)
|
||||
#expect(transport.cancelledTransfers == ["failed-clear-outgoing"])
|
||||
#expect(viewModel.messageIDToTransferId[outgoingID] == nil)
|
||||
}
|
||||
@@ -1446,9 +1457,12 @@ struct ChatViewModelPrivateMediaDeletionTests {
|
||||
transport.deletedPrivateMediaRelativePaths
|
||||
== [[incomingID: "images/incoming/\(filename)"]]
|
||||
)
|
||||
let messages = viewModel.privateChats[peerID] ?? []
|
||||
#expect(messages.prefix(2).map(\.id) == [incomingID, outgoingID])
|
||||
#expect(messages.last?.sender == "system")
|
||||
#expect(
|
||||
viewModel.privateChats[peerID]?.map(\.id)
|
||||
== [incomingID, outgoingID]
|
||||
messages.last?.content
|
||||
== String(localized: "content.system.media_delete_refused")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user