Route oversize send failure to the active conversation

Addresses review feedback: the size-cap error was posted to the main
mesh timeline, so a user sending from a private chat or channel never
saw it. rejectIfOversized now posts to the private conversation
(addPrivateMessageNoUnread) or channel (addChannelMessage) the send
originated from, falling back to the main timeline for public sends.

Adds regression tests for both routings.
This commit is contained in:
callebtc 2026-07-27 22:24:02 +02:00
parent c3bc911bcd
commit 0721c39f89
2 changed files with 70 additions and 9 deletions

View File

@ -82,18 +82,42 @@ class MediaSendingManager(
private var pendingAutomaticTimeoutRequestId: String? = null
/**
* Enforce the send-size cap with a user-visible failure.
* Returns true if the file is oversized and the send was aborted.
* Enforce the send-size cap with a user-visible failure posted to the
* conversation the user is sending from. Returns true if the file is
* oversized and the send was aborted.
*/
private fun rejectIfOversized(file: java.io.File): Boolean {
private fun rejectIfOversized(
file: java.io.File,
toPeerIDOrNull: String?,
channelOrNull: String?
): Boolean {
val size = file.length()
if (size <= MAX_FILE_SIZE) return false
Log.e(TAG, "❌ File too large: $size bytes (max: $MAX_FILE_SIZE)")
val sizeMb = size / (1024 * 1024)
val maxMb = MAX_FILE_SIZE / (1024 * 1024)
messageManager.addSystemMessage(
"cannot send ${file.name}: file is too large (${sizeMb} MB, max $maxMb MB)"
)
val text = "cannot send ${file.name}: file is too large (${sizeMb} MB, max $maxMb MB)"
when {
toPeerIDOrNull != null -> {
val sys = BitchatMessage(
sender = "system",
content = text,
timestamp = Date(),
isRelay = false
)
messageManager.addPrivateMessageNoUnread(toPeerIDOrNull, sys)
}
channelOrNull != null -> {
val sys = BitchatMessage(
sender = "system",
content = text,
timestamp = Date(),
isRelay = false
)
messageManager.addChannelMessage(channelOrNull, sys)
}
else -> messageManager.addSystemMessage(text)
}
return true
}
@ -119,7 +143,7 @@ class MediaSendingManager(
return@withContext null
}
if (rejectIfOversized(file)) {
if (rejectIfOversized(file, toPeerIDOrNull, channelOrNull)) {
return@withContext null
}
@ -163,7 +187,7 @@ class MediaSendingManager(
return@withContext null
}
if (rejectIfOversized(file)) {
if (rejectIfOversized(file, toPeerIDOrNull, channelOrNull)) {
return@withContext null
}
@ -207,7 +231,7 @@ class MediaSendingManager(
return@withContext null
}
if (rejectIfOversized(file)) {
if (rejectIfOversized(file, toPeerIDOrNull, channelOrNull)) {
return@withContext null
}

View File

@ -287,6 +287,43 @@ class MediaSendingManagerMigrationTest {
assertTrue(messages.none { it.type == com.bitchat.android.model.BitchatMessageType.Image })
}
@Test
fun `oversized file failure is posted to the private conversation and nothing is sent`() {
val bigFile = kotlin.io.path.createTempFile("oversized-private", ".jpg").toFile()
try {
bigFile.writeBytes(ByteArray(11 * 1024 * 1024) { 0x42 })
manager.sendImageNote(peerID, null, bigFile.absolutePath)
val messages = state.privateChats.value[peerID].orEmpty()
assertEquals(1, messages.size)
assertTrue(messages.single().content.contains("too large"))
assertTrue(messages.none { it.type == com.bitchat.android.model.BitchatMessageType.Image })
assertTrue(state.getMessagesValue().none { it.content.contains("too large") })
verify(mesh, never()).prepareFilePrivate(any(), any(), any(), any())
} finally {
bigFile.delete()
}
}
@Test
fun `oversized file failure is posted to the channel and nothing is sent`() {
val bigFile = kotlin.io.path.createTempFile("oversized-channel", ".jpg").toFile()
try {
bigFile.writeBytes(ByteArray(11 * 1024 * 1024) { 0x42 })
manager.sendImageNote(null, "#test", bigFile.absolutePath)
val channelMessages = state.getChannelMessagesValue()["#test"].orEmpty()
assertEquals(1, channelMessages.size)
assertTrue(channelMessages.single().content.contains("too large"))
assertTrue(state.getMessagesValue().none { it.content.contains("too large") })
verify(mesh, never()).prepareFilePrivate(any(), any(), any(), any())
} finally {
bigFile.delete()
}
}
@Test
fun `cancelled consent cannot later send or echo`() {
whenever(mesh.prepareFilePrivate(eq(peerID), any(), any(), eq(false)))