Merge remote-tracking branch 'origin/HEAD' into less_memory_test_branch

This commit is contained in:
Vitor Pamplona
2023-03-09 15:42:22 -05:00
24 changed files with 1207 additions and 216 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.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<IdentityClaim>) {
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)
}
@@ -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<IdentityClaim>, 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<IdentityClaim>, privateKey: ByteArray, createdAt: Long = Date().time / 1000): MetadataEvent {
val content = contactMetaData
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 sig = Utils.sign(id, privateKey)
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.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
)
}
}
}
@@ -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 = ""
}
}
@@ -1,11 +1,14 @@
package com.vitorpamplona.amethyst.ui.components
import androidx.compose.foundation.background
import android.content.res.Configuration
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.Divider
import androidx.compose.material.Icon
@@ -17,6 +20,7 @@ import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
@@ -24,18 +28,27 @@ import com.vitorpamplona.amethyst.R
@Composable
fun SelectTextDialog(text: String, onDismiss: () -> Unit) {
val screenHeight = LocalConfiguration.current.screenHeightDp.dp
val maxHeight =
if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) {
screenHeight * 0.6f
} else {
screenHeight * 0.9f
}
Dialog(
onDismissRequest = onDismiss
) {
Card {
Column {
Column(
modifier = Modifier.heightIn(24.dp, maxHeight)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End
) {
IconButton(
onClick = onDismiss,
modifier = Modifier.background(MaterialTheme.colors.background)
onClick = onDismiss
) {
Icon(
imageVector = Icons.Default.ArrowBack,
@@ -46,9 +59,13 @@ fun SelectTextDialog(text: String, onDismiss: () -> Unit) {
Text(text = stringResource(R.string.select_text_dialog_top))
}
Divider()
Row(modifier = Modifier.padding(16.dp)) {
SelectionContainer {
Text(text)
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
Row(modifier = Modifier.padding(16.dp)) {
SelectionContainer {
Text(text)
}
}
}
}
@@ -1,5 +1,6 @@
package com.vitorpamplona.amethyst.ui.note
import android.content.Intent
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
@@ -25,11 +26,13 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.navigation.NavController
import coil.compose.AsyncImage
import com.google.accompanist.flowlayout.FlowRow
@@ -753,7 +756,8 @@ fun UserPicture(
@Composable
fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current.applicationContext
val appContext = LocalContext.current.applicationContext
val actContext = LocalContext.current
DropdownMenu(
expanded = popupExpanded,
@@ -769,6 +773,33 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
}
Divider()
}
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(accountViewModel.decrypt(note) ?: "")); onDismiss() }) {
Text(stringResource(R.string.copy_text))
}
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString("@${note.author?.pubkeyNpub()}" ?: "")); onDismiss() }) {
Text(stringResource(R.string.copy_user_pubkey))
}
DropdownMenuItem(onClick = { clipboardManager.setText(AnnotatedString(note.idNote())); onDismiss() }) {
Text(stringResource(R.string.copy_note_id))
}
DropdownMenuItem(onClick = {
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(
Intent.EXTRA_TEXT,
externalLinkForNote(note)
)
putExtra(Intent.EXTRA_TITLE, actContext.getString(R.string.quick_action_share_browser_link))
}
val shareIntent = Intent.createChooser(sendIntent, appContext.getString(R.string.quick_action_share))
ContextCompat.startActivity(actContext, shareIntent, null)
onDismiss()
}) {
Text(stringResource(R.string.quick_action_share))
}
Divider()
DropdownMenuItem(onClick = { accountViewModel.broadcast(note); onDismiss() }) {
Text(stringResource(R.string.broadcast))
}
@@ -784,7 +815,7 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
note.author?.let {
accountViewModel.hide(
it,
context
appContext
)
}; onDismiss()
}) {
@@ -793,35 +824,35 @@ fun NoteDropDownMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit,
Divider()
DropdownMenuItem(onClick = {
accountViewModel.report(note, ReportEvent.ReportType.SPAM)
note.author?.let { accountViewModel.hide(it, context) }
note.author?.let { accountViewModel.hide(it, appContext) }
onDismiss()
}) {
Text(stringResource(R.string.report_spam_scam))
}
DropdownMenuItem(onClick = {
accountViewModel.report(note, ReportEvent.ReportType.PROFANITY)
note.author?.let { accountViewModel.hide(it, context) }
note.author?.let { accountViewModel.hide(it, appContext) }
onDismiss()
}) {
Text(stringResource(R.string.report_hateful_speech))
}
DropdownMenuItem(onClick = {
accountViewModel.report(note, ReportEvent.ReportType.IMPERSONATION)
note.author?.let { accountViewModel.hide(it, context) }
note.author?.let { accountViewModel.hide(it, appContext) }
onDismiss()
}) {
Text(stringResource(R.string.report_impersonation))
}
DropdownMenuItem(onClick = {
accountViewModel.report(note, ReportEvent.ReportType.NUDITY)
note.author?.let { accountViewModel.hide(it, context) }
note.author?.let { accountViewModel.hide(it, appContext) }
onDismiss()
}) {
Text(stringResource(R.string.report_nudity_porn))
}
DropdownMenuItem(onClick = {
accountViewModel.report(note, ReportEvent.ReportType.ILLEGAL)
note.author?.let { accountViewModel.hide(it, context) }
note.author?.let { accountViewModel.hide(it, appContext) }
onDismiss()
}) {
Text(stringResource(R.string.report_illegal_behaviour))
@@ -47,6 +47,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Popup
@@ -81,7 +82,7 @@ fun VerticalDivider(color: Color) =
@Composable
fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Unit, accountViewModel: AccountViewModel) {
val context = LocalContext.current
val primaryLight = lightenColor(MaterialTheme.colors.primary, 0.2f)
val primaryLight = lightenColor(MaterialTheme.colors.primary, 0.1f)
val cardShape = RoundedCornerShape(5.dp)
val clipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope()
@@ -186,7 +187,6 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
ContextCompat.startActivity(context, shareIntent, null)
onDismiss()
}
VerticalDivider(primaryLight)
}
}
}
@@ -237,7 +237,7 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
fun NoteQuickActionItem(icon: ImageVector, label: String, onClick: () -> Unit) {
Column(
modifier = Modifier
.size(64.dp)
.size(70.dp)
.clickable { onClick() },
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
@@ -245,9 +245,9 @@ fun NoteQuickActionItem(icon: ImageVector, label: String, onClick: () -> Unit) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.onPrimary
modifier = Modifier.size(24.dp).padding(bottom = 5.dp),
tint = Color.White
)
Text(text = label, fontSize = 12.sp)
Text(text = label, fontSize = 12.sp, color = Color.White, textAlign = TextAlign.Center)
}
}
@@ -9,6 +9,7 @@ import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt
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.MoreVert
@@ -58,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
@@ -349,20 +351,21 @@ private fun ProfileHeader(
) {
MessageButton(baseUser, navController)
NPubCopyButton(baseUser)
// No need for this button anymore
// NPubCopyButton(baseUser)
if (accountUser == baseUser) {
EditButton(account)
} else {
if (account.isHidden(baseUser)) {
ShowUserButton {
account.showUser(baseUser.pubkeyHex)
}
} else if (accountUser.isFollowing(baseUser)) {
UnfollowButton { coroutineScope.launch(Dispatchers.IO) { account.unfollow(baseUser) } }
} else {
FollowButton { coroutineScope.launch(Dispatchers.IO) { account.follow(baseUser) } }
}
if (account.isHidden(baseUser)) {
ShowUserButton {
account.showUser(baseUser.pubkeyHex)
}
} else if (accountUser.isFollowing(baseUser)) {
UnfollowButton { coroutineScope.launch(Dispatchers.IO) { account.unfollow(baseUser) } }
} else {
FollowButton { coroutineScope.launch(Dispatchers.IO) { account.follow(baseUser) } }
}
}
}
@@ -388,6 +391,7 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, navController:
val userBadge = userBadgeState?.user ?: return
val uri = LocalUriHandler.current
val clipboardManager = LocalClipboardManager.current
Row(verticalAlignment = Alignment.Bottom) {
user.bestDisplayName()?.let {
@@ -408,6 +412,44 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, navController:
}
}
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = user.pubkeyDisplayHex(),
modifier = Modifier.padding(top = 1.dp, bottom = 1.dp),
color = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
IconButton(
modifier = Modifier.size(30.dp).padding(start = 5.dp),
onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); }
) {
Icon(
imageVector = Icons.Default.ContentCopy,
null,
modifier = Modifier.padding(end = 5.dp).size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
}
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
@@ -461,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)
)
}
}
}