diff --git a/app/src/main/java/com/bitchat/android/ui/CashuTokenDecoder.kt b/app/src/main/java/com/bitchat/android/ui/CashuTokenDecoder.kt index ee17ffa1..0daf401c 100644 --- a/app/src/main/java/com/bitchat/android/ui/CashuTokenDecoder.kt +++ b/app/src/main/java/com/bitchat/android/ui/CashuTokenDecoder.kt @@ -47,7 +47,7 @@ object CashuTokenDecoder { } if (token.length !in 12..MAX_TOKEN_LENGTH) return null if (!token.startsWith("cashuA") && !token.startsWith("cashuB")) return null - if (token.any { !it.isLetterOrDigit() && it !in "-_+/=." }) return null + if (token.any { !it.isLetterOrDigit() && it !in "-_+/=" }) return null return token } @@ -198,7 +198,7 @@ object CashuTokenDecoder { }.getOrNull()?.takeIf { it >= 0 } } - private val TOKEN_REGEX = Regex("""(?i:cashu:(?://)?)?cashu[AB][A-Za-z0-9_+/.=%-]{6,}""") + private val TOKEN_REGEX = Regex("""(?i:cashu:(?://)?)?cashu[AB][A-Za-z0-9_+/%=-]{6,}""") } private sealed interface CborValue { diff --git a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt index 968cd154..61c88523 100644 --- a/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt +++ b/app/src/main/java/com/bitchat/android/ui/ChatViewModel.kt @@ -920,6 +920,20 @@ class ChatViewModel( mesh.myPeerID, state.getNicknameValue() ) + } else if (channel != null && channelManager.hasChannelKey(channel)) { + channelManager.sendEncryptedChannelMessage( + messageContent, + mentions, + channel, + state.getNicknameValue(), + mesh.myPeerID, + onEncryptedPayload = { + mesh.sendMessage(messageContent, mentions, channel) + }, + onFallback = { + mesh.sendMessage(messageContent, mentions, channel) + } + ) } else { mesh.sendMessage(messageContent, mentions, channel) } diff --git a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt index ab5235d9..b10ac9e1 100644 --- a/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt +++ b/app/src/main/java/com/bitchat/android/ui/CommandProcessor.kt @@ -438,11 +438,18 @@ class CommandProcessor( isRelay = false ) val selectedPeer = state.getSelectedPrivateChatPeerValue() + val selectedLocationChannel = state.selectedLocationChannel.value val channel = state.getCurrentChannelValue() when { selectedPeer != null -> { messageManager.addPrivateMessageNoUnread(selectedPeer, message.copy(isPrivate = true)) } + selectedLocationChannel is com.bitchat.android.geohash.ChannelID.Location -> { + messageManager.addChannelMessage( + "geo:${selectedLocationChannel.channel.geohash}", + message + ) + } channel != null -> channelManager.addChannelMessage(channel, message, null) else -> messageManager.addMessage(message) } diff --git a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt index 5202d156..5ab113db 100644 --- a/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt +++ b/app/src/main/java/com/bitchat/android/ui/MessageComponents.kt @@ -1,5 +1,11 @@ package com.bitchat.android.ui +import android.content.ActivityNotFoundException +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.net.Uri import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.animation.AnimatedContent @@ -15,7 +21,9 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -25,13 +33,17 @@ import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ColorScheme +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -53,6 +65,8 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp diff --git a/app/src/test/java/com/bitchat/android/ui/CashuTokenDecoderTest.kt b/app/src/test/java/com/bitchat/android/ui/CashuTokenDecoderTest.kt index eb20f62c..8514d088 100644 --- a/app/src/test/java/com/bitchat/android/ui/CashuTokenDecoderTest.kt +++ b/app/src/test/java/com/bitchat/android/ui/CashuTokenDecoderTest.kt @@ -94,6 +94,14 @@ class CashuTokenDecoderTest { assertEquals(first, CashuTokenDecoder.bareToken("cashu%3A$first")) } + @Test + fun `sentence punctuation is not included in extracted token`() { + val token = v3Token("""{"token":[{"proofs":[{"amount":1}]}]}""") + + assertEquals(listOf(token), CashuTokenDecoder.extractTokens("Redeem $token.")) + assertNull(CashuTokenDecoder.bareToken("$token.")) + } + @Test fun `oversized and deeply nested input fails closed`() { assertNull(CashuTokenDecoder.decode("cashuA" + "A".repeat(CashuTokenDecoder.MAX_TOKEN_LENGTH))) diff --git a/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt b/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt index 9eff027c..011f80a2 100644 --- a/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt +++ b/app/src/test/java/com/bitchat/android/ui/CommandProcessorTest.kt @@ -2,6 +2,9 @@ package com.bitchat.android.ui import android.content.Context import androidx.test.core.app.ApplicationProvider +import com.bitchat.android.geohash.ChannelID +import com.bitchat.android.geohash.GeohashChannel +import com.bitchat.android.geohash.GeohashChannelLevel import com.bitchat.android.mesh.MeshService import com.bitchat.android.model.BitchatMessage import junit.framework.TestCase.assertEquals @@ -136,4 +139,26 @@ class CommandProcessorTest() { assertTrue(locallyRead.contains(message.id)) } + + @Test + fun `pay feedback is added to active geohash channel`() { + val geohash = "u0nd" + chatState.setSelectedLocationChannel( + ChannelID.Location(GeohashChannel(GeohashChannelLevel.PROVINCE, geohash)) + ) + + commandProcessor.processCommand( + command = "/pay invalid", + meshService = meshService, + myPeerID = "peer-id", + onSendMessage = { _, _, _ -> }, + viewModel = null + ) + + assertEquals( + "invalid cashu token — not sending it", + chatState.getChannelMessagesValue()["geo:$geohash"]?.single()?.content + ) + assertEquals(0, chatState.getMessagesValue().size) + } }