From 3b582636f42c2c461dd0c1c479c8c629dfa5eec3 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 9 Mar 2023 13:24:32 -0500 Subject: [PATCH] NIP-39 Support --- .../vitorpamplona/amethyst/model/Account.kt | 5 +- .../amethyst/service/model/MetadataEvent.kt | 143 +++++++- .../ui/actions/NewUserMetadataView.kt | 313 ++++++++++-------- .../ui/actions/NewUserMetadataViewModel.kt | 49 ++- .../ui/screen/loggedIn/ProfileScreen.kt | 50 ++- app/src/main/res/drawable/github.xml | 4 + app/src/main/res/drawable/mastodon.xml | 14 + app/src/main/res/drawable/telegram.xml | 6 + app/src/main/res/drawable/twitter.xml | 7 + app/src/main/res/values/strings.xml | 13 + 10 files changed, 452 insertions(+), 152 deletions(-) create mode 100644 app/src/main/res/drawable/github.xml create mode 100644 app/src/main/res/drawable/mastodon.xml create mode 100644 app/src/main/res/drawable/telegram.xml create mode 100644 app/src/main/res/drawable/twitter.xml 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 92a9a7f1c..9645fc180 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/model/Account.kt @@ -9,6 +9,7 @@ import com.vitorpamplona.amethyst.service.model.ChannelMetadataEvent import com.vitorpamplona.amethyst.service.model.Contact import com.vitorpamplona.amethyst.service.model.ContactListEvent 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.MetadataEvent import com.vitorpamplona.amethyst.service.model.PrivateDmEvent @@ -106,11 +107,11 @@ class Account( } } - fun sendNewUserMetadata(toString: String) { + fun sendNewUserMetadata(toString: String, identities: List) { if (!isWriteable()) return loggedIn.privKey?.let { - val event = MetadataEvent.create(toString, loggedIn.privKey!!) + val event = MetadataEvent.create(toString, identities, loggedIn.privKey!!) Client.send(event) LocalCache.consume(event) } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt index 312d3ca43..b3da7c803 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/service/model/MetadataEvent.kt @@ -2,6 +2,7 @@ package com.vitorpamplona.amethyst.service.model import android.util.Log import com.google.gson.Gson +import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.HexKey import com.vitorpamplona.amethyst.model.toHexKey import nostr.postr.Utils @@ -14,6 +15,127 @@ data class ContactMetaData( 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( id: HexKey, pubKey: HexKey, @@ -29,18 +151,31 @@ class MetadataEvent( 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 { const val kind = 0 val gson = Gson() - fun create(contactMetaData: ContactMetaData, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent { - return create(gson.toJson(contactMetaData), privateKey, createdAt = createdAt) + fun create(contactMetaData: ContactMetaData, identities: List, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent { + 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, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent { val content = contactMetaData val pubKey = Utils.pubkeyCreate(privateKey).toHexKey() - val tags = listOf>() + val tags = mutableListOf>() + identities?.forEach { + tags.add(listOf("i", it.platformIdentity(), it.proof)) + } + val id = generateId(pubKey, createdAt, kind, tags, content) val sig = Utils.sign(id, privateKey) return MetadataEvent(id.toHexKey(), pubKey, createdAt, tags, content, sig.toHexKey()) 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 ac9bd2aea..3a683ec89 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 @@ -7,7 +7,9 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll import androidx.compose.material.MaterialTheme import androidx.compose.material.OutlinedTextField import androidx.compose.material.Surface @@ -15,7 +17,6 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource 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.model.Account -@OptIn(ExperimentalComposeUiApi::class) @Composable fun NewUserMetadataView(onClose: () -> Unit, account: Account) { 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( - label = { Text(text = stringResource(R.string.display_name)) }, - modifier = Modifier.weight(1f), - value = postViewModel.displayName.value, - onValueChange = { postViewModel.displayName.value = it }, + 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(R.string.my_display_name), + text = stringResource(id = R.string.about_me), color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) ) }, keyboardOptions = KeyboardOptions.Default.copy( capitalization = KeyboardCapitalization.Sentences ), - singleLine = true + maxLines = 10 ) - Text("@", Modifier.padding(5.dp)) + Spacer(modifier = Modifier.height(10.dp)) OutlinedTextField( - label = { Text(text = stringResource(R.string.username)) }, - modifier = Modifier.weight(1f), - value = postViewModel.userName.value, - onValueChange = { postViewModel.userName.value = it }, + label = { Text(text = stringResource(R.string.avatar_url)) }, + modifier = Modifier.fillMaxWidth(), + value = postViewModel.picture.value, + onValueChange = { postViewModel.picture.value = it }, placeholder = { Text( - text = stringResource(R.string.my_username), + 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) + ) + } + ) + + 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 - ) } } } 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 a7857b953..0b8e272ff 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 @@ -5,6 +5,9 @@ import androidx.lifecycle.ViewModel import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ObjectNode 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.StringWriter @@ -23,6 +26,10 @@ class NewUserMetadataViewModel : ViewModel() { val lnAddress = mutableStateOf("") val lnURL = mutableStateOf("") + val twitter = mutableStateOf("") + val github = mutableStateOf("") + val mastodon = mutableStateOf("") + fun load(account: Account) { this.account = account @@ -36,6 +43,19 @@ class NewUserMetadataViewModel : ViewModel() { nip05.value = it.info?.nip05 ?: "" lnAddress.value = it.info?.lud16 ?: "" 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("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() ObjectMapper().writeValue(writer, currentJson) - account.sendNewUserMetadata(writer.buffer.toString()) + account.sendNewUserMetadata(writer.buffer.toString(), newClaims) clear() } @@ -79,5 +123,8 @@ class NewUserMetadataViewModel : ViewModel() { nip05.value = "" lnAddress.value = "" lnURL.value = "" + twitter.value = "" + github.value = "" + mastodon.value = "" } } 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 5a7753827..387423e87 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 @@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.service.NostrUserProfileDataSource import com.vitorpamplona.amethyst.service.model.BadgeDefinitionEvent 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.ui.actions.NewUserMetadataView 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) val website = user.info?.website @@ -484,20 +503,25 @@ 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() + val identities = user.info?.latestMetadata?.identityClaims() + if (!identities.isNullOrEmpty()) { + identities.forEach { identity: IdentityClaim -> + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + tint = Color.Unspecified, + painter = painterResource(id = identity.toIcon()), + contentDescription = stringResource(identity.toDescriptor()), + modifier = Modifier.size(16.dp) + ) - if (baseBadgeDefinition != null) { - BadgeThumb(baseBadgeDefinition, navController, 50.dp) - } - } - } + ClickableText( + 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) + ) } } } diff --git a/app/src/main/res/drawable/github.xml b/app/src/main/res/drawable/github.xml new file mode 100644 index 000000000..e3e491850 --- /dev/null +++ b/app/src/main/res/drawable/github.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/mastodon.xml b/app/src/main/res/drawable/mastodon.xml new file mode 100644 index 000000000..c4eae624d --- /dev/null +++ b/app/src/main/res/drawable/mastodon.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/telegram.xml b/app/src/main/res/drawable/telegram.xml new file mode 100644 index 000000000..0032dc154 --- /dev/null +++ b/app/src/main/res/drawable/telegram.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/drawable/twitter.xml b/app/src/main/res/drawable/twitter.xml new file mode 100644 index 000000000..82cf785b6 --- /dev/null +++ b/app/src/main/res/drawable/twitter.xml @@ -0,0 +1,7 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 72a8a38ad..f3c2a9d95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -208,4 +208,17 @@ Follow Request Deletion 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. + + Github Gist w/ Proof + Telegram + Mastodon Post ID w/ Proof + Twitter Status w/ Proof + + https://gist.github.com/<user>/<gist> + https://t.me/<proof post> + https://<server>/<user>/<proof post> + https://twitter.com/<user>/status/<proof post> + + +