NIP-39 Support

This commit is contained in:
Vitor Pamplona
2023-03-09 13:24:32 -05:00
parent 56433a3ad3
commit 3b582636f4
10 changed files with 452 additions and 152 deletions
@@ -9,6 +9,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent
import com.vitorpamplona.amethyst.service.model.Contact import com.vitorpamplona.amethyst.service.model.Contact
import com.vitorpamplona.amethyst.service.model.ContactListEvent import com.vitorpamplona.amethyst.service.model.ContactListEvent
import com.vitorpamplona.amethyst.service.model.DeletionEvent import com.vitorpamplona.amethyst.service.model.DeletionEvent
import com.vitorpamplona.amethyst.service.model.IdentityClaim
import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent import com.vitorpamplona.amethyst.service.model.LnZapRequestEvent
import com.vitorpamplona.amethyst.service.model.MetadataEvent import com.vitorpamplona.amethyst.service.model.MetadataEvent
import com.vitorpamplona.amethyst.service.model.PrivateDmEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent
@@ -106,11 +107,11 @@ class Account(
} }
} }
fun sendNewUserMetadata(toString: String) { fun sendNewUserMetadata(toString: String, identities: List<IdentityClaim>) {
if (!isWriteable()) return if (!isWriteable()) return
loggedIn.privKey?.let { loggedIn.privKey?.let {
val event = MetadataEvent.create(toString, loggedIn.privKey!!) val event = MetadataEvent.create(toString, identities, loggedIn.privKey!!)
Client.send(event) Client.send(event)
LocalCache.consume(event) LocalCache.consume(event)
} }
@@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service.model
import android.util.Log import android.util.Log
import com.google.gson.Gson import com.google.gson.Gson
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.toHexKey import com.vitorpamplona.amethyst.model.toHexKey
import nostr.postr.Utils import nostr.postr.Utils
@@ -14,6 +15,127 @@ data class ContactMetaData(
val nip05: String? val nip05: String?
) )
abstract class IdentityClaim(
var identity: String,
var proof: String
) {
abstract fun toProofUrl(): String
abstract fun toIcon(): Int
abstract fun toDescriptor(): Int
abstract fun platform(): String
fun platformIdentity() = "${platform()}:$identity"
companion object {
fun create(platformIdentity: String, proof: String): IdentityClaim? {
val platformIdentity = platformIdentity.split(':')
val platform = platformIdentity[0]
val identity = platformIdentity[1]
return when (platform.lowercase()) {
GitHubIdentity.platform -> GitHubIdentity(identity, proof)
TwitterIdentity.platform -> TwitterIdentity(identity, proof)
TelegramIdentity.platform -> TelegramIdentity(identity, proof)
MastodonIdentity.platform -> MastodonIdentity(identity, proof)
else -> throw IllegalArgumentException("Platform $platform not supported")
}
}
}
}
class GitHubIdentity(
identity: String,
proof: String
) : IdentityClaim(identity, proof) {
override fun toProofUrl() = "https://gist.github.com/$identity/$proof"
override fun platform() = platform
override fun toIcon() = R.drawable.github
override fun toDescriptor() = R.string.github
companion object {
val platform = "github"
fun parseProofUrl(proofUrl: String): GitHubIdentity? {
return try {
if (proofUrl.isBlank()) return null
val path = proofUrl.removePrefix("https://gist.github.com/").split("?")[0].split("/")
GitHubIdentity(path[0], path[1])
} catch (e: Exception) {
null
}
}
}
}
class TwitterIdentity(
identity: String,
proof: String
) : IdentityClaim(identity, proof) {
override fun toProofUrl() = "https://twitter.com/$identity/status/$proof"
override fun platform() = platform
override fun toIcon() = R.drawable.twitter
override fun toDescriptor() = R.string.twitter
companion object {
val platform = "twitter"
fun parseProofUrl(proofUrl: String): TwitterIdentity? {
return try {
if (proofUrl.isBlank()) return null
val path = proofUrl.removePrefix("https://twitter.com/").split("?")[0].split("/")
TwitterIdentity(path[0], path[2])
} catch (e: Exception) {
null
}
}
}
}
class TelegramIdentity(
identity: String,
proof: String
) : IdentityClaim(identity, proof) {
override fun toProofUrl() = "https://t.me/$proof"
override fun platform() = platform
override fun toIcon() = R.drawable.telegram
override fun toDescriptor() = R.string.telegram
companion object {
val platform = "telegram"
}
}
class MastodonIdentity(
identity: String,
proof: String
) : IdentityClaim(identity, proof) {
override fun toProofUrl() = "https://$identity/$proof"
override fun platform() = platform
override fun toIcon() = R.drawable.mastodon
override fun toDescriptor() = R.string.mastodon
companion object {
val platform = "mastodon"
fun parseProofUrl(proofUrl: String): MastodonIdentity? {
return try {
if (proofUrl.isBlank()) return null
val path = proofUrl.removePrefix("https://").split("?")[0].split("/")
return MastodonIdentity(path[0], path[1])
} catch (e: Exception) {
null
}
}
}
}
class MetadataEvent( class MetadataEvent(
id: HexKey, id: HexKey,
pubKey: HexKey, pubKey: HexKey,
@@ -29,18 +151,31 @@ class MetadataEvent(
null null
} }
fun identityClaims() = tags.filter { it.firstOrNull() == "i" }.mapNotNull {
try {
IdentityClaim.create(it.get(1), it.get(2))
} catch (e: Exception) {
Log.e("MetadataEvent", "Can't parse identity [${it.joinToString { "," }}]", e)
null
}
}
companion object { companion object {
const val kind = 0 const val kind = 0
val gson = Gson() val gson = Gson()
fun create(contactMetaData: ContactMetaData, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent { fun create(contactMetaData: ContactMetaData, identities: List<IdentityClaim>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent {
return create(gson.toJson(contactMetaData), privateKey, createdAt = createdAt) return create(gson.toJson(contactMetaData), identities, privateKey, createdAt = createdAt)
} }
fun create(contactMetaData: String, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent { fun create(contactMetaData: String, identities: List<IdentityClaim>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent {
val content = contactMetaData val content = contactMetaData
val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() val pubKey = Utils.pubkeyCreate(privateKey).toHexKey()
val tags = listOf<List<String>>() val tags = mutableListOf<List<String>>()
identities?.forEach {
tags.add(listOf("i", it.platformIdentity(), it.proof))
}
val id = generateId(pubKey, createdAt, kind, tags, content) val id = generateId(pubKey, createdAt, kind, tags, content)
val sig = Utils.sign(id, privateKey) val sig = Utils.sign(id, privateKey)
return MetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) return MetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey())
@@ -7,7 +7,9 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Surface import androidx.compose.material.Surface
@@ -15,7 +17,6 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
@@ -26,7 +27,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
@OptIn(ExperimentalComposeUiApi::class)
@Composable @Composable
fun NewUserMetadataView(onClose: () -> Unit, account: Account) { fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
val postViewModel: NewUserMetadataViewModel = viewModel() val postViewModel: NewUserMetadataViewModel = viewModel()
@@ -66,158 +66,207 @@ fun NewUserMetadataView(onClose: () -> Unit, account: Account) {
) )
} }
Spacer(modifier = Modifier.height(10.dp)) Column(
modifier = Modifier.padding(10.dp).verticalScroll(rememberScrollState())
) {
Row(
modifier = Modifier.fillMaxWidth(1f),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
label = { Text(text = stringResource(R.string.display_name)) },
modifier = Modifier.weight(1f),
value = postViewModel.displayName.value,
onValueChange = { postViewModel.displayName.value = it },
placeholder = {
Text(
text = stringResource(R.string.my_display_name),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
singleLine = true
)
Text("@", Modifier.padding(5.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.username)) },
modifier = Modifier.weight(1f),
value = postViewModel.userName.value,
onValueChange = { postViewModel.userName.value = it },
placeholder = {
Text(
text = stringResource(R.string.my_username),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true
)
}
Spacer(modifier = Modifier.height(10.dp))
Row(modifier = Modifier.fillMaxWidth(1f), verticalAlignment = Alignment.CenterVertically) {
OutlinedTextField( OutlinedTextField(
label = { Text(text = stringResource(R.string.display_name)) }, label = { Text(text = stringResource(R.string.about_me)) },
modifier = Modifier.weight(1f), modifier = Modifier
value = postViewModel.displayName.value, .fillMaxWidth()
onValueChange = { postViewModel.displayName.value = it }, .height(100.dp),
value = postViewModel.about.value,
onValueChange = { postViewModel.about.value = it },
placeholder = { placeholder = {
Text( Text(
text = stringResource(R.string.my_display_name), text = stringResource(id = R.string.about_me),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
) )
}, },
keyboardOptions = KeyboardOptions.Default.copy( keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences capitalization = KeyboardCapitalization.Sentences
), ),
singleLine = true maxLines = 10
) )
Text("@", Modifier.padding(5.dp)) Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField( OutlinedTextField(
label = { Text(text = stringResource(R.string.username)) }, label = { Text(text = stringResource(R.string.avatar_url)) },
modifier = Modifier.weight(1f), modifier = Modifier.fillMaxWidth(),
value = postViewModel.userName.value, value = postViewModel.picture.value,
onValueChange = { postViewModel.userName.value = it }, onValueChange = { postViewModel.picture.value = it },
placeholder = { placeholder = {
Text( Text(
text = stringResource(R.string.my_username), text = "https://mywebsite.com/me.jpg",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
) )
}, },
singleLine = true singleLine = true
) )
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.banner_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.banner.value,
onValueChange = { postViewModel.banner.value = it },
placeholder = {
Text(
text = "https://mywebsite.com/mybanner.jpg",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.website_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.website.value,
onValueChange = { postViewModel.website.value = it },
placeholder = {
Text(
text = "https://mywebsite.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.nip_05)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.nip05.value,
onValueChange = { postViewModel.nip05.value = it },
placeholder = {
Text(
text = "_@mywebsite.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.ln_address)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.lnAddress.value,
onValueChange = { postViewModel.lnAddress.value = it },
placeholder = {
Text(
text = "me@mylightiningnode.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.ln_url_outdated)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.lnURL.value,
onValueChange = { postViewModel.lnURL.value = it },
placeholder = {
Text(
text = stringResource(R.string.lnurl),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.twitter)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.twitter.value,
onValueChange = { postViewModel.twitter.value = it },
placeholder = {
Text(
text = stringResource(R.string.twitter_proof_url_template),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.mastodon)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.mastodon.value,
onValueChange = { postViewModel.mastodon.value = it },
placeholder = {
Text(
text = stringResource(R.string.mastodon_proof_url_template),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.github)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.github.value,
onValueChange = { postViewModel.github.value = it },
placeholder = {
Text(
text = stringResource(R.string.github_proof_url_template),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
)
} }
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.about_me)) },
modifier = Modifier
.fillMaxWidth()
.height(100.dp),
value = postViewModel.about.value,
onValueChange = { postViewModel.about.value = it },
placeholder = {
Text(
text = stringResource(id = R.string.about_me),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences
),
maxLines = 10
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.avatar_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.picture.value,
onValueChange = { postViewModel.picture.value = it },
placeholder = {
Text(
text = "https://mywebsite.com/me.jpg",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.banner_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.banner.value,
onValueChange = { postViewModel.banner.value = it },
placeholder = {
Text(
text = "https://mywebsite.com/mybanner.jpg",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.website_url)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.website.value,
onValueChange = { postViewModel.website.value = it },
placeholder = {
Text(
text = "https://mywebsite.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.nip_05)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.nip05.value,
onValueChange = { postViewModel.nip05.value = it },
placeholder = {
Text(
text = "_@mywebsite.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.ln_address)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.lnAddress.value,
onValueChange = { postViewModel.lnAddress.value = it },
placeholder = {
Text(
text = "me@mylightiningnode.com",
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true
)
Spacer(modifier = Modifier.height(10.dp))
OutlinedTextField(
label = { Text(text = stringResource(R.string.ln_url_outdated)) },
modifier = Modifier.fillMaxWidth(),
value = postViewModel.lnURL.value,
onValueChange = { postViewModel.lnURL.value = it },
placeholder = {
Text(
text = stringResource(R.string.lnurl),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
},
singleLine = true
)
} }
} }
} }
@@ -5,6 +5,9 @@ import androidx.lifecycle.ViewModel
import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.ObjectNode
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.service.model.GitHubIdentity
import com.vitorpamplona.amethyst.service.model.MastodonIdentity
import com.vitorpamplona.amethyst.service.model.TwitterIdentity
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.StringWriter import java.io.StringWriter
@@ -23,6 +26,10 @@ class NewUserMetadataViewModel : ViewModel() {
val lnAddress = mutableStateOf("") val lnAddress = mutableStateOf("")
val lnURL = mutableStateOf("") val lnURL = mutableStateOf("")
val twitter = mutableStateOf("")
val github = mutableStateOf("")
val mastodon = mutableStateOf("")
fun load(account: Account) { fun load(account: Account) {
this.account = account this.account = account
@@ -36,6 +43,19 @@ class NewUserMetadataViewModel : ViewModel() {
nip05.value = it.info?.nip05 ?: "" nip05.value = it.info?.nip05 ?: ""
lnAddress.value = it.info?.lud16 ?: "" lnAddress.value = it.info?.lud16 ?: ""
lnURL.value = it.info?.lud06 ?: "" lnURL.value = it.info?.lud06 ?: ""
twitter.value = ""
github.value = ""
mastodon.value = ""
// TODO: Validate Telegram input, somehow.
it.info?.latestMetadata?.identityClaims()?.forEach {
when (it) {
is TwitterIdentity -> twitter.value = it.toProofUrl()
is GitHubIdentity -> github.value = it.toProofUrl()
is MastodonIdentity -> mastodon.value = it.toProofUrl()
}
}
} }
} }
@@ -61,10 +81,34 @@ class NewUserMetadataViewModel : ViewModel() {
currentJson.put("lud16", lnAddress.value.trim()) currentJson.put("lud16", lnAddress.value.trim())
currentJson.put("lud06", lnURL.value.trim()) currentJson.put("lud06", lnURL.value.trim())
var claims = latest?.identityClaims() ?: emptyList()
if (twitter.value.isBlank()) {
// delete twitter
claims = claims.filter { it !is TwitterIdentity }
}
if (github.value.isBlank()) {
// delete github
claims = claims.filter { it !is GitHubIdentity }
}
if (mastodon.value.isBlank()) {
// delete mastodon
claims = claims.filter { it !is MastodonIdentity }
}
// Updates while keeping other identities intact
val newClaims = listOfNotNull(
TwitterIdentity.parseProofUrl(twitter.value),
GitHubIdentity.parseProofUrl(github.value),
MastodonIdentity.parseProofUrl(mastodon.value)
) + claims.filter { it !is TwitterIdentity && it !is GitHubIdentity && it !is MastodonIdentity }
val writer = StringWriter() val writer = StringWriter()
ObjectMapper().writeValue(writer, currentJson) ObjectMapper().writeValue(writer, currentJson)
account.sendNewUserMetadata(writer.buffer.toString()) account.sendNewUserMetadata(writer.buffer.toString(), newClaims)
clear() clear()
} }
@@ -79,5 +123,8 @@ class NewUserMetadataViewModel : ViewModel() {
nip05.value = "" nip05.value = ""
lnAddress.value = "" lnAddress.value = ""
lnURL.value = "" lnURL.value = ""
twitter.value = ""
github.value = ""
mastodon.value = ""
} }
} }
@@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource
import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent
import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent import com.vitorpamplona.amethyst.service.model.BadgeProfilesEvent
import com.vitorpamplona.amethyst.service.model.IdentityClaim
import com.vitorpamplona.amethyst.service.model.ReportEvent import com.vitorpamplona.amethyst.service.model.ReportEvent
import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView import com.vitorpamplona.amethyst.ui.actions.NewUserMetadataView
import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy import com.vitorpamplona.amethyst.ui.components.AsyncImageProxy
@@ -431,6 +432,24 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, navController:
} }
} }
userBadge.acceptedBadges?.let { note ->
(note.event as? BadgeProfilesEvent)?.let { event ->
FlowRow(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) {
event.badgeAwardEvents().forEach { badgeAwardEvent ->
val baseNote = LocalCache.notes[badgeAwardEvent]
if (baseNote != null) {
val badgeAwardState by baseNote.live().metadata.observeAsState()
val baseBadgeDefinition = badgeAwardState?.note?.replyTo?.firstOrNull()
if (baseBadgeDefinition != null) {
BadgeThumb(baseBadgeDefinition, navController, 50.dp)
}
}
}
}
}
}
DisplayNip05ProfileStatus(user) DisplayNip05ProfileStatus(user)
val website = user.info?.website val website = user.info?.website
@@ -484,20 +503,25 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, navController:
} }
} }
userBadge.acceptedBadges?.let { note -> val identities = user.info?.latestMetadata?.identityClaims()
(note.event as? BadgeProfilesEvent)?.let { event -> if (!identities.isNullOrEmpty()) {
FlowRow(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(vertical = 5.dp)) { identities.forEach { identity: IdentityClaim ->
event.badgeAwardEvents().forEach { badgeAwardEvent -> Row(verticalAlignment = Alignment.CenterVertically) {
val baseNote = LocalCache.notes[badgeAwardEvent] Icon(
if (baseNote != null) { tint = Color.Unspecified,
val badgeAwardState by baseNote.live().metadata.observeAsState() painter = painterResource(id = identity.toIcon()),
val baseBadgeDefinition = badgeAwardState?.note?.replyTo?.firstOrNull() contentDescription = stringResource(identity.toDescriptor()),
modifier = Modifier.size(16.dp)
)
if (baseBadgeDefinition != null) { ClickableText(
BadgeThumb(baseBadgeDefinition, navController, 50.dp) text = AnnotatedString(identity.identity),
} onClick = { runCatching { uri.openUri(identity.toProofUrl()) } },
} style = LocalTextStyle.current.copy(color = MaterialTheme.colors.primary),
} modifier = Modifier
.padding(top = 1.dp, bottom = 1.dp, start = 5.dp)
.weight(1f)
)
} }
} }
} }
+4
View File
@@ -0,0 +1,4 @@
<vector android:height="128dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="128dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#3A6CAC" android:fillType="evenOdd" android:pathData="M296.13,354.17c49.88,-5.89 102.94,-24.03 102.94,-110.19c0,-24.49 -8.62,-44.45 -22.67,-59.87c2.27,-5.89 9.52,-28.11 -2.73,-58.95c0,0 -18.14,-5.9 -60.76,22.67c-18.14,-4.98 -38.09,-8.16 -56.68,-8.16c-19.05,0 -39.01,3.18 -56.7,8.16c-43.08,-28.57 -61.22,-22.67 -61.22,-22.67c-12.24,30.83 -4.98,53.06 -2.72,58.95c-14.06,15.42 -22.68,35.38 -22.68,59.87c0,86.16 53.06,104.3 102.94,110.19c-6.34,5.45 -12.24,15.87 -14.51,30.39c-12.7,5.44 -45.81,15.87 -65.76,-18.59c0,0 -11.8,-21.31 -34.01,-22.67c0,0 -22.22,-0.45 -1.81,13.59c0,0 14.96,6.81 24.94,32.65c0,0 13.6,43.09 76.18,29.48v38.54c0,5.91 -4.53,12.7 -15.86,10.89C96.14,438.98 32.2,354.63 32.2,255.77c0,-123.81 100.22,-224.02 224.03,-224.02c123.35,0 224.02,100.22 223.57,224.02c0,98.86 -63.95,182.75 -152.83,212.69c-11.34,2.27 -15.87,-4.53 -15.87,-10.89V395.45C311.1,374.58 304.29,360.98 296.13,354.17L296.13,354.17zM512,256.23C512,114.73 397.26,0 256.23,0C114.73,0 0,114.73 0,256.23C0,397.26 114.73,512 256.23,512C397.26,512 512,397.26 512,256.23L512,256.23z"/>
</vector>
+14
View File
@@ -0,0 +1,14 @@
<vector android:height="128dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="128dp"
xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:pathData="M317,381q-124,28 -123,-39 69,15 149,2 67,-13 72,-80 3,-101 -3,-116 -19,-49 -72,-58 -98,-10 -162,0 -56,10 -75,58 -12,31 -3,147 3,32 9,53 13,46 70,69 83,23 138,-9">
<aapt:attr name="android:fillColor">
<gradient android:endX="418" android:endY="440"
android:startX="91" android:startY="80" android:type="linear">
<item android:color="#FF6364FF" android:offset="0"/>
<item android:color="#FF563ACC" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
<path android:fillColor="#fff" android:pathData="M360,293h-36v-93q-1,-26 -29,-23 -20,3 -20,34v47h-36v-47q0,-31 -20,-34 -30,-3 -30,28v88h-36v-91q1,-51 44,-60 33,-5 51,21l9,15 9,-15q16,-26 51,-21 43,9 43,60"/>
</vector>
+6
View File
@@ -0,0 +1,6 @@
<vector android:height="128dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="128dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#c8daea" android:pathData="M199,404c-11,0 -10,-4 -13,-14l-32,-105 245,-144"/>
<path android:fillColor="#a9c9dd" android:pathData="M199,404c7,0 11,-4 16,-8l45,-43 -56,-34"/>
<path android:fillColor="#37aee2" android:pathData="M204,319l135,99c14,9 26,4 30,-14l55,-258c5,-22 -9,-32 -24,-25L79,245c-21,8 -21,21 -4,26l83,26 190,-121c9,-5 17,-3 11,4"/>
</vector>
+7
View File
@@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="128dp" android:viewportHeight="512"
android:viewportWidth="512" android:width="128dp">
<path
android:pathData="M437,152a72,72 0,0 1,-40 12a72,72 0,0 0,32 -40a72,72 0,0 1,-45 17a72,72 0,0 0,-122 65a200,200 0,0 1,-145 -74a72,72 0,0 0,22 94a72,72 0,0 1,-32 -7a72,72 0,0 0,56 69a72,72 0,0 1,-32 1a72,72 0,0 0,67 50a200,200 0,0 1,-105 29a200,200 0,0 0,309 -179a200,200 0,0 0,35 -37"
android:fillColor="#1da1f2"/>
</vector>
+13
View File
@@ -208,4 +208,17 @@
<string name="quick_action_follow">Follow</string> <string name="quick_action_follow">Follow</string>
<string name="quick_action_request_deletion_alert_title">Request Deletion</string> <string name="quick_action_request_deletion_alert_title">Request Deletion</string>
<string name="quick_action_request_deletion_alert_body">Amethyst will request that your note be deleted from the relays you are currently connected to. There is no guarantee that your note will be permanently deleted from those relays, or from other relays where it may be stored.</string> <string name="quick_action_request_deletion_alert_body">Amethyst will request that your note be deleted from the relays you are currently connected to. There is no guarantee that your note will be permanently deleted from those relays, or from other relays where it may be stored.</string>
<string name="github" translatable="false">Github Gist w/ Proof</string>
<string name="telegram" translatable="false">Telegram</string>
<string name="mastodon" translatable="false">Mastodon Post ID w/ Proof</string>
<string name="twitter" translatable="false">Twitter Status w/ Proof</string>
<string name="github_proof_url_template" translatable="false">https://gist.github.com/&lt;user&gt;/&lt;gist&gt;</string>
<string name="telegram_proof_url_template" translatable="false">https://t.me/&lt;proof post&gt;</string>
<string name="mastodon_proof_url_template" translatable="false">https://&lt;server&gt;/&lt;user&gt;/&lt;proof post&gt;</string>
<string name="twitter_proof_url_template" translatable="false">https://twitter.com/&lt;user&gt;/status/&lt;proof post&gt;</string>
</resources> </resources>