diff --git a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt index 22f17867..fd1251f3 100644 --- a/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt +++ b/app/src/main/java/com/bitchat/android/mesh/BluetoothMeshService.kt @@ -852,6 +852,34 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic return reusable } + + /** + * Mesh timeline stays UTF-8 for iOS interop. Channel messages use the shared + * [BitchatMessage] binary envelope so receivers can route by `channel`. + */ + private fun encodePublicOrChannelPayload( + content: String, + mentions: List, + channel: String? + ): ByteArray { + if (channel == null) return content.toByteArray(Charsets.UTF_8) + val nickname = try { + com.bitchat.android.services.NicknameProvider.getNickname(context, myPeerID) + } catch (_: Exception) { + myPeerID + } + val encoded = BitchatMessage( + sender = nickname, + content = content, + timestamp = java.util.Date(), + isRelay = false, + senderPeerID = myPeerID, + mentions = mentions.takeIf { it.isNotEmpty() }, + channel = channel + ).toBinaryPayload() + return encoded ?: content.toByteArray(Charsets.UTF_8) + } + /** * Send public message */ @@ -859,13 +887,14 @@ class BluetoothMeshService(private val context: Context) : TransportBridgeServic if (content.isEmpty()) return serviceScope.launch { + val payloadBytes = encodePublicOrChannelPayload(content, mentions, channel) val packet = BitchatPacket( version = 1u, type = MessageType.MESSAGE.value, senderID = hexStringToByteArray(myPeerID), recipientID = SpecialRecipients.BROADCAST, timestamp = System.currentTimeMillis().toULong(), - payload = content.toByteArray(Charsets.UTF_8), + payload = payloadBytes, signature = null, ttl = MAX_TTL ) diff --git a/app/src/main/java/com/bitchat/android/mesh/MeshCore.kt b/app/src/main/java/com/bitchat/android/mesh/MeshCore.kt index 28cd3d02..c84f74fd 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MeshCore.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MeshCore.kt @@ -530,16 +530,42 @@ class MeshCore( } } + + /** + * Mesh timeline stays UTF-8 for iOS interop. Channel messages use the shared + * [BitchatMessage] binary envelope so receivers can route by `channel` instead + * of dumping every #room into the public Mesh feed. + */ + private fun encodePublicOrChannelPayload( + content: String, + mentions: List, + channel: String? + ): ByteArray { + if (channel == null) return content.toByteArray(Charsets.UTF_8) + val nickname = hooks.announcementNicknameProvider?.invoke() ?: myPeerID + val encoded = BitchatMessage( + sender = nickname, + content = content, + timestamp = java.util.Date(), + isRelay = false, + senderPeerID = myPeerID, + mentions = mentions.takeIf { it.isNotEmpty() }, + channel = channel + ).toBinaryPayload() + return encoded ?: content.toByteArray(Charsets.UTF_8) + } + fun sendMessage(content: String, mentions: List = emptyList(), channel: String? = null) { if (content.isEmpty()) return scope.launch { + val payloadBytes = encodePublicOrChannelPayload(content, mentions, channel) val packet = BitchatPacket( version = 1u, type = MessageType.MESSAGE.value, senderID = MeshPacketUtils.hexStringToByteArray(myPeerID), recipientID = SpecialRecipients.BROADCAST, timestamp = System.currentTimeMillis().toULong(), - payload = content.toByteArray(Charsets.UTF_8), + payload = payloadBytes, signature = null, ttl = maxTtl ) diff --git a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt index 9434fc6e..ff96b63a 100644 --- a/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt +++ b/app/src/main/java/com/bitchat/android/mesh/MessageHandler.kt @@ -484,10 +484,20 @@ class MessageHandler(private val myPeerID: String, private val appContext: andro Log.w(TAG, "FILE_TRANSFER decode failed (broadcast) from ${peerID.take(8)}") } - // Fallback: plain text - val message = BitchatMessage( - id = PacketIdUtil.computeIdHex(packet).uppercase(), - sender = delegate?.getPeerNickname(peerID) ?: "unknown", + // Prefer the shared binary envelope (channel / mentions). Fall back to + // legacy UTF-8 mesh text for older peers and for iOS public broadcasts. + val packetId = PacketIdUtil.computeIdHex(packet).uppercase() + val peerNickname = delegate?.getPeerNickname(peerID) ?: "unknown" + val message = BitchatMessage.fromBinaryPayload(packet.payload)?.let { decoded -> + decoded.copy( + id = packetId, + sender = decoded.sender.ifBlank { peerNickname }, + senderPeerID = peerID, + timestamp = Date(packet.timestamp.toLong()) + ) + } ?: BitchatMessage( + id = packetId, + sender = peerNickname, content = String(packet.payload, Charsets.UTF_8), senderPeerID = peerID, timestamp = Date(packet.timestamp.toLong()) diff --git a/app/src/test/kotlin/com/bitchat/android/model/ChannelWireFormatTest.kt b/app/src/test/kotlin/com/bitchat/android/model/ChannelWireFormatTest.kt new file mode 100644 index 00000000..22816239 --- /dev/null +++ b/app/src/test/kotlin/com/bitchat/android/model/ChannelWireFormatTest.kt @@ -0,0 +1,37 @@ +package com.bitchat.android.model + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Test +import java.util.Date + +class ChannelWireFormatTest { + @Test + fun channelFlagSurvivesBinaryRoundTrip() { + val original = BitchatMessage( + sender = "alice", + content = "hello channel", + timestamp = Date(1_700_000_000_000L), + senderPeerID = "aabbccddeeff0011", + channel = "#general" + ) + val encoded = original.toBinaryPayload() + assertNotNull(encoded) + // Must not be plain UTF-8 of the content alone — that is the mesh leak. + assertTrue(encoded!!.size > original.content.toByteArray(Charsets.UTF_8).size) + + val decoded = BitchatMessage.fromBinaryPayload(encoded) + assertNotNull(decoded) + assertEquals("#general", decoded!!.channel) + assertEquals("hello channel", decoded.content) + assertEquals("alice", decoded.sender) + } + + @Test + fun plainUtf8MeshPayloadDoesNotParseAsChannelEnvelope() { + val plain = "just a mesh hello".toByteArray(Charsets.UTF_8) + assertNull(BitchatMessage.fromBinaryPayload(plain)) + } +}