diff --git a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt index 0a3057932..03eb6da78 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -555,6 +555,7 @@ class Account( nip05: String? = null, lnAddress: String? = null, lnURL: String? = null, + simplex: String? = null, twitter: String? = null, mastodon: String? = null, github: String? = null, @@ -571,6 +572,7 @@ class Account( nip05 = nip05, lnAddress = lnAddress, lnURL = lnURL, + simplex = simplex, moneroAddress = moneroAddress, twitter = twitter, mastodon = mastodon, diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataView.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataView.kt index dd0446d20..755b9fecd 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataView.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataView.kt @@ -259,6 +259,22 @@ fun NewUserMetadataView( Spacer(modifier = Modifier.height(10.dp)) + OutlinedTextField( + label = { Text(text = "SimpleX") }, + modifier = Modifier.fillMaxWidth(), + value = postViewModel.simplex.value, + onValueChange = { postViewModel.simplex.value = it }, + placeholder = { + Text( + text = "simplex:/...", + color = MaterialTheme.colorScheme.placeholderText, + ) + }, + singleLine = true, + ) + + Spacer(modifier = Modifier.height(10.dp)) + OutlinedTextField( label = { Text(text = stringResource(R.string.twitter)) }, modifier = Modifier.fillMaxWidth(), diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt index 02dc193f1..ac9a74b19 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/actions/NewUserMetadataViewModel.kt @@ -53,6 +53,7 @@ class NewUserMetadataViewModel : ViewModel() { val nip05 = mutableStateOf("") val lnAddress = mutableStateOf("") val lnURL = mutableStateOf("") + val simplex = mutableStateOf("") val twitter = mutableStateOf("") val github = mutableStateOf("") @@ -76,6 +77,7 @@ class NewUserMetadataViewModel : ViewModel() { nip05.value = it.info?.nip05 ?: "" lnAddress.value = it.info?.lud16 ?: "" lnURL.value = it.info?.lud06 ?: "" + simplex.value = it.info?.simplex ?: "" twitter.value = "" github.value = "" @@ -104,6 +106,7 @@ class NewUserMetadataViewModel : ViewModel() { nip05 = nip05.value, lnAddress = lnAddress.value, lnURL = lnURL.value, + simplex = simplex.value, twitter = twitter.value, mastodon = mastodon.value, github = github.value, @@ -122,6 +125,7 @@ class NewUserMetadataViewModel : ViewModel() { nip05.value = "" lnAddress.value = "" lnURL.value = "" + simplex.value = "" twitter.value = "" github.value = "" mastodon.value = "" diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt index abb8a8b3f..2e02a7c71 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/ReactionsRow.kt @@ -124,6 +124,7 @@ import com.vitorpamplona.amethyst.ui.theme.Font14SP import com.vitorpamplona.amethyst.ui.theme.HalfDoubleVertSpacer import com.vitorpamplona.amethyst.ui.theme.Height24dpModifier import com.vitorpamplona.amethyst.ui.theme.ModifierWidth3dp +import com.vitorpamplona.amethyst.ui.theme.MoneroOrange import com.vitorpamplona.amethyst.ui.theme.NoSoTinyBorders import com.vitorpamplona.amethyst.ui.theme.ReactionRowExpandButton import com.vitorpamplona.amethyst.ui.theme.ReactionRowHeight @@ -226,7 +227,21 @@ private fun InnerReactionRow( ZapReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav = nav) }, six = { - TipReaction(baseNote, MaterialTheme.colorScheme.placeholderText, accountViewModel, nav = nav) + val author = baseNote.author + val authorState = author?.live()?.metadata?.observeAsState() + + val hasMoneroAddress = + remember(authorState?.value) { + !author?.info?.moneroAddress().isNullOrBlank() + } + val tipTintColor = + if (hasMoneroAddress) { + MoneroOrange + } else { + MaterialTheme.colorScheme.placeholderText + } + + TipReaction(baseNote, tipTintColor, accountViewModel, nav = nav) }, seven = { ViewCountReaction( diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt index 326a1ba54..3fe7c363b 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/ProfileScreen.kt @@ -54,6 +54,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.EditNote import androidx.compose.material.icons.filled.Link +import androidx.compose.material.icons.filled.Message import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults @@ -1115,6 +1116,9 @@ private fun DrawAdditionalInfo( val moneroAddress = remember { baseUser.info?.moneroAddress() } DisplayMoneroAddress(address = moneroAddress, userHex = pubkeyHex, accountViewModel = accountViewModel, nav) + val simplex = remember { baseUser.info?.simplex } + DisplaySimpleXAddress(address = simplex, accountViewModel = accountViewModel) + val identities = user.latestMetadata?.identityClaims() if (!identities.isNullOrEmpty()) { identities.forEach { identity: IdentityClaim -> @@ -1327,6 +1331,40 @@ fun DisplayMoneroAddress( } } +@Composable +fun DisplaySimpleXAddress( + address: String?, + accountViewModel: AccountViewModel, +) { + val uri = LocalUriHandler.current + + if (!address.isNullOrEmpty()) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Default.Message, + contentDescription = "SimpleX", + modifier = Size16Modifier, + tint = MaterialTheme.colorScheme.primary, + ) + + ClickableText( + text = AnnotatedString(address), + onClick = { + val finalUri = if (address.startsWith("simplex:") || address.startsWith("http")) address else "simplex:$address" + runCatching { uri.openUri(finalUri) } + }, + style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.primary), + overflow = TextOverflow.Ellipsis, + softWrap = false, + modifier = + Modifier + .padding(top = 1.dp, bottom = 1.dp, start = 5.dp) + .weight(1f), + ) + } + } +} + @Composable @OptIn(ExperimentalLayoutApi::class) private fun DisplayAppRecommendations( diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt index c64424bce..83f33567c 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/ContactListEvent.kt @@ -435,6 +435,8 @@ class UserMetadata { return lud16 ?: lud06 } + var simplex: String? = null + fun moneroAddress(): String? { return cryptoAddresses?.get("monero") } @@ -463,6 +465,7 @@ class UserMetadata { if (banner?.isNotEmpty() == true) banner = banner?.trim() if (website?.isNotEmpty() == true) website = website?.trim() if (domain?.isNotEmpty() == true) domain = domain?.trim() + if (simplex?.isNotEmpty() == true) simplex = simplex?.trim() if (picture?.isBlank() == true) picture = null if (nip05?.isBlank() == true) nip05 = null @@ -475,6 +478,7 @@ class UserMetadata { if (banner?.isBlank() == true) banner = null if (website?.isBlank() == true) website = null if (domain?.isBlank() == true) domain = null + if (simplex?.isBlank() == true) simplex = null } } diff --git a/quartz/src/main/java/com/vitorpamplona/quartz/events/MetadataEvent.kt b/quartz/src/main/java/com/vitorpamplona/quartz/events/MetadataEvent.kt index ba2db3399..fe505ef44 100644 --- a/quartz/src/main/java/com/vitorpamplona/quartz/events/MetadataEvent.kt +++ b/quartz/src/main/java/com/vitorpamplona/quartz/events/MetadataEvent.kt @@ -187,6 +187,7 @@ class MetadataEvent( lnAddress: String?, lnURL: String?, moneroAddress: String?, + simplex: String?, twitter: String?, mastodon: String?, github: String?, @@ -214,6 +215,7 @@ class MetadataEvent( nip05?.let { addIfNotBlank(currentJson, "nip05", it.trim()) } lnAddress?.let { addIfNotBlank(currentJson, "lud16", it.trim()) } lnURL?.let { addIfNotBlank(currentJson, "lud06", it.trim()) } + simplex?.let { addIfNotBlank(currentJson, "simplex", it.trim()) } moneroAddress?.let { val cryptos = if (currentJson.has("cryptocurrency_addresses")) { diff --git a/quartz/src/test/java/com/vitorpamplona/quartz/events/GarnetFeaturesTest.kt b/quartz/src/test/java/com/vitorpamplona/quartz/events/GarnetFeaturesTest.kt new file mode 100644 index 000000000..eaad1a334 --- /dev/null +++ b/quartz/src/test/java/com/vitorpamplona/quartz/events/GarnetFeaturesTest.kt @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2024 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.quartz.events + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +class GarnetFeaturesTest { + private val mapper = jacksonObjectMapper() + + @Test + fun testMoneroAddressRetrieval() { + val metadata = UserMetadata() + metadata.cryptoAddresses = mapOf("monero" to "44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXz9ucDeZaYo7kn7sZyj55xXLTxtr5W3e3v6kK8e3S9qV8R8") + + assertEquals("44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXz9ucDeZaYo7kn7sZyj55xXLTxtr5W3e3v6kK8e3S9qV8R8", metadata.moneroAddress()) + } + + @Test + fun testMoneroAddressNull() { + val metadata = UserMetadata() + assertNull(metadata.moneroAddress()) + + metadata.cryptoAddresses = mapOf("bitcoin" to "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa") + assertNull(metadata.moneroAddress()) + } + + @Test + fun testSimplexFieldProcessing() { + val metadata = UserMetadata() + metadata.simplex = " simplex:/smp/server/id " + + // Before cleaning, it should have the spaces + assertEquals(" simplex:/smp/server/id ", metadata.simplex) + + metadata.cleanBlankNames() + + // After cleaning, it should be trimmed + assertEquals("simplex:/smp/server/id", metadata.simplex) + } + + @Test + fun testSimplexFieldNullification() { + val metadata = UserMetadata() + metadata.simplex = " " + + metadata.cleanBlankNames() + + // Should be nullified if blank + assertNull(metadata.simplex) + } + + @Test + fun testMetadataSerializationWithSimplex() { + // Mocking a JSON string that represents a MetadataEvent's content + val json = + """ + { + "name": "User", + "simplex": "simplex:/smp/server/id" + } + """.trimIndent() + + val metadata = mapper.readValue(json, UserMetadata::class.java) + assertEquals("simplex:/smp/server/id", metadata.simplex) + } + + @Test + fun testMetadataSerializationWithMonero() { + val json = + """ + { + "name": "User", + "cryptocurrency_addresses": { + "monero": "44AFF" + } + } + """.trimIndent() + + val metadata = mapper.readValue(json, UserMetadata::class.java) + assertEquals("44AFF", metadata.moneroAddress()) + } +}