mirror of
https://github.com/permissionlesstech/bitchat.git
synced 2026-07-26 23:45:19 +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",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"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" : {
|
"notification.action.wave" : {
|
||||||
"comment" : "Title of the notification action button that sends a friendly wave back to a nearby person",
|
"comment" : "Title of the notification action button that sends a friendly wave back to a nearby person",
|
||||||
"extractionState" : "manual",
|
"extractionState" : "manual",
|
||||||
|
|||||||
@@ -91,6 +91,9 @@ protocol ChatMediaTransferContext: AnyObject {
|
|||||||
func removeUntombstonedMediaMessage(withID messageID: String)
|
func removeUntombstonedMediaMessage(withID messageID: String)
|
||||||
func removeOutgoingMediaMessage(withID messageID: String)
|
func removeOutgoingMediaMessage(withID messageID: String)
|
||||||
func addSystemMessage(_ content: 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()`).
|
/// Signals that message state changed so observers refresh (e.g. `objectWillChange.send()`).
|
||||||
func notifyUIChanged()
|
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(
|
private func isIncomingPrivateMessage(
|
||||||
_ message: BitchatMessage
|
_ message: BitchatMessage
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
@@ -1176,6 +1201,9 @@ final class ChatMediaTransferCoordinator {
|
|||||||
"Refusing to delete private media without a durable tombstone id=\(messageID.prefix(12))…",
|
"Refusing to delete private media without a durable tombstone id=\(messageID.prefix(12))…",
|
||||||
category: .session
|
category: .session
|
||||||
)
|
)
|
||||||
|
self.context.notifyMediaDeletionRefused(
|
||||||
|
messageID: messageID
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.finishMediaDeletion(
|
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))…",
|
"Refusing to clear private chat without durable media tombstones peer=\(peerID.id.prefix(8))…",
|
||||||
category: .session
|
category: .session
|
||||||
)
|
)
|
||||||
|
notifyPrivateMediaDeletionRefused(peerID: peerID)
|
||||||
completion()
|
completion()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,10 @@ private final class MockChatMediaTransferContext: ChatMediaTransferContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func addSystemMessage(_ content: String) { systemMessages.append(content) }
|
func addSystemMessage(_ content: String) { systemMessages.append(content) }
|
||||||
|
private(set) var mediaDeletionRefusals: [String] = []
|
||||||
|
func notifyMediaDeletionRefused(messageID: String) {
|
||||||
|
mediaDeletionRefusals.append(messageID)
|
||||||
|
}
|
||||||
func notifyUIChanged() { notifyUIChangedCount += 1 }
|
func notifyUIChanged() { notifyUIChangedCount += 1 }
|
||||||
|
|
||||||
// Delivery status & dedup
|
// Delivery status & dedup
|
||||||
@@ -643,6 +647,8 @@ struct ChatMediaTransferCoordinatorContextTests {
|
|||||||
#expect(context.removedMessages.isEmpty)
|
#expect(context.removedMessages.isEmpty)
|
||||||
#expect(context.cancelledTransfers == ["failed-delete"])
|
#expect(context.cancelledTransfers == ["failed-delete"])
|
||||||
#expect(coordinator.messageIDToTransferId[messageID] == nil)
|
#expect(coordinator.messageIDToTransferId[messageID] == nil)
|
||||||
|
// The refusal must be visible in the affected chat, not just logged.
|
||||||
|
#expect(context.mediaDeletionRefusals == [messageID])
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test @MainActor
|
@Test @MainActor
|
||||||
|
|||||||
@@ -1272,9 +1272,13 @@ struct ChatViewModelPrivateMediaDeletionTests {
|
|||||||
"images/incoming/\(filename)"
|
"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(
|
#expect(
|
||||||
viewModel.privateChats[peerID]?.map(\.id)
|
messages.last?.content
|
||||||
== [stableID, legacyID]
|
== String(localized: "content.system.media_delete_refused")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1374,10 +1378,17 @@ struct ChatViewModelPrivateMediaDeletionTests {
|
|||||||
#expect(
|
#expect(
|
||||||
transport.deletedPrivateMediaMessageIDBatches == [[incomingID]]
|
transport.deletedPrivateMediaMessageIDBatches == [[incomingID]]
|
||||||
)
|
)
|
||||||
|
let messages = viewModel.privateChats[peerID] ?? []
|
||||||
#expect(
|
#expect(
|
||||||
viewModel.privateChats[peerID]?.map(\.id)
|
messages.prefix(3).map(\.id)
|
||||||
== [incomingID, outgoingID, "ordinary-message"]
|
== [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(transport.cancelledTransfers == ["failed-clear-outgoing"])
|
||||||
#expect(viewModel.messageIDToTransferId[outgoingID] == nil)
|
#expect(viewModel.messageIDToTransferId[outgoingID] == nil)
|
||||||
}
|
}
|
||||||
@@ -1446,9 +1457,12 @@ struct ChatViewModelPrivateMediaDeletionTests {
|
|||||||
transport.deletedPrivateMediaRelativePaths
|
transport.deletedPrivateMediaRelativePaths
|
||||||
== [[incomingID: "images/incoming/\(filename)"]]
|
== [[incomingID: "images/incoming/\(filename)"]]
|
||||||
)
|
)
|
||||||
|
let messages = viewModel.privateChats[peerID] ?? []
|
||||||
|
#expect(messages.prefix(2).map(\.id) == [incomingID, outgoingID])
|
||||||
|
#expect(messages.last?.sender == "system")
|
||||||
#expect(
|
#expect(
|
||||||
viewModel.privateChats[peerID]?.map(\.id)
|
messages.last?.content
|
||||||
== [incomingID, outgoingID]
|
== String(localized: "content.system.media_delete_refused")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user