diff --git a/app/build.gradle b/app/build.gradle index 32ab8260a..878ecefcd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,8 +12,8 @@ android { applicationId "com.vitorpamplona.amethyst" minSdk 26 targetSdk 33 - versionCode 89 - versionName "0.24.0" + versionCode 90 + versionName "0.24.1" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { 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 0299bb4cb..42d631421 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/components/SelectTextDialog.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt index bd9d488b9..245c7c6f8 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/components/SelectTextDialog.kt @@ -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) + } } } } diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt index 1a1bae8c6..87f675c08 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteCompose.kt @@ -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)) diff --git a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt index bf2d910d2..d03997ab6 100644 --- a/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt +++ b/app/src/main/java/com/vitorpamplona/amethyst/ui/note/NoteQuickActionMenu.kt @@ -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) } } 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 13f94df4d..02ea533ae 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 @@ -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) + ) } } } 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-es/strings.xml b/app/src/main/res/values-es/strings.xml index e9e9869ad..1418e84b4 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,7 +1,20 @@ - Amethyst - Amethyst Debug + Marcar los conocidos como leídos + Marcar los nuevos como leídos + Marcar todos como leídos + Clave secreta (nsec) copiada al portapapeles + Copiar mi clave secreta + Autenticación fallida + Error + Credo por %1$s + Imagen del Badge para %1$s + Has recibido un nuevo Badge + Badge otorgado a + Texto de nota copiado al portapapeles + \@npub del autor copiado al portapapeles + ID de nota copiada (@note1) al portapapeles + Seleccionar texto Apunta al código QR Mostrar el QR Imagen de Perfil @@ -80,6 +93,7 @@ Fuente de mensajes privados Fuente de chat público Fuente global + Fuente de búsqueda Anadir transmisor Nombre para mostrar Mi nombre para mostrar @@ -155,15 +169,12 @@ Chat público publicaciones recibidas Eliminar - sats Auto traducido de a Mostrar en primero "Traducir siempre a " - NIP-05 - LNURL... nunca ahora h @@ -174,4 +185,16 @@ Denuncia discurso de odio Reportar desnudos / Porno otros + Selecciona + Compartir enlace del navegador + Compartir + ID del autor + ID de la nota + Copiar texto + Eliminar + Dejar de seguir + Seguir + Solicitar eliminación + Amethyst solicitará que se elimine su nota de los relays a los que está conectado actualmente. No hay garantía de que su nota se elimine permanentemente de esos relays, o de otros relays donde pueda almacenarse. + Claves de respaldo diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index e9f0687ff..3ae6ec542 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,7 +1,5 @@ - Amethyst - Amethyst Debug Pointer vers le QR code Montrer le QR code Image de profil @@ -80,6 +78,7 @@ Flux de messages privés Flux de chat public Flux mondial + Flux de recherche Ajouter un relais Nom visible du public Mon nom visible du public @@ -155,15 +154,12 @@ Chat public messages reçus Retirer - sats Auto traduit de vers Montre en en premier "Toujours traduire en " - NIP-05 - LNURL... jamais maintenant h @@ -182,4 +178,5 @@ \n- **Conservez** une sauvegarde sécurisée de votre clé secrète pour la récupération de compte. Nous vous recommandons d\'utiliser un gestionnaire de mots de passe. Clé secrète (nsec) copiée dans le presse-papiers + Clés de sauvegarde diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 33c91736a..276da1fc3 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -1,7 +1,5 @@ - Amethyst - Amethyst Debug Mutass a QR kódra QR kód megjelenítése Profil kép @@ -156,15 +154,12 @@ Publikus Chat beérkezett hozzászólások Eltávolítás - sats Automatikus fordítás erről erre Mutasd ebben első Mindig fordítsa le - NIP-05 - LNURL... soha most ó diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 2ae02417c..1333bb192 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -78,6 +78,7 @@ Feed de mensagens privadas Feed Chat Público Feed Global + Fonte de pesquisa Adicionar um Relay Nome de Exibição Meu nome de exibição diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 3ee249dac..94213bdf1 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1,6 +1,4 @@ - Amethyst - Amethyst Debug Наведите на QR код Показать QR Фото профиля @@ -160,8 +158,6 @@ Показать сперва на Всегда переводить на - NIP-05 - LNURL... никогда сейчас ч diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index d94dbd541..1743f1efc 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,7 +1,5 @@ - Amethyst - Amethyst Debug QR Code\'a yönlendir QR Göster Profil Resmi @@ -154,15 +152,12 @@ Herkese Açık Chat alınan gönderiler Kaldır - satlar Otomatik şundan çevirildi şuna Şunda göster ilk Her zaman şuna çevir - NIP-05 - LNURL asla şimdi h diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 49a468613..9ecf49a25 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -1,6 +1,4 @@ - Amethyst - Amethyst Debug Наведіть на QR код Показати QR Фото профілю @@ -160,8 +158,6 @@ Показати спершу на Завжди перекладати на - NIP-05 - LNURL... ніколи зараз год diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml new file mode 100644 index 000000000..15a3c5002 --- /dev/null +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -0,0 +1,206 @@ + + 對準二維碼 + 顯示二維碼 + 頭像圖片 + 掃描二維碼 + 一直顯示 + 帖文被標記為不當 + 未找到帖子 + 頻道圖片 + 未找到相關事件 + 無法解密消息 + 羣聊圖片 + 明確內容 + 垃圾郵件 + 冒充 + 非法行為 + 未知 + 中繼器圖標 + 未知作者 + 複製文本 + 複製作者@npub + 複製筆記ID + 廣播 + + 舉報垃圾郵件/詐騙 + 舉報冒充 + 舉報明確內容 + 舉報非法行為 + 使用私鑰登錄以便回覆 + 使用私鑰登錄以便提升帖子 + 使用私鑰登錄以便點贊帖子 + 沒有設置Zap金額。長按以更改 + 使用私鑰登錄以便發送Zaps + Zaps + 瀏覽次數 + 提升 + 已提升 + 引用 + 新的聰金額 + 添加 + "回覆 " + " 和 " + " 在頻道 " + 個人檔案橫幅 + " 關注" + " 粉絲" + 個人檔案 + 安全過濾器 + 登出 + 顯示更多 + 閃電網絡發票 + 付款 + 閃電小費 + 聰給收款方的留言 + 非常感謝! + 聰數量 + 發送聰 + "不再翻譯" + "解析 %1$s 預覽出錯:%2$s" + "預覽 %1$s 卡片圖片" + 新頻道 + 頻道名 + 我的精彩羣聊 + 圖片鏈接 + 描述 + "關於我們.. " + 你在想什麼? + 發佈 + 保存 + 創建 + 取消 + 上傳圖片失敗 + 中繼器地址 + 帖文 + 錯誤 + 主頁 + 私信 + 公開聊天 + 全球 + 搜索 + 添加中繼器 + 顯示名稱 + 我的顯示名稱 + 用户名 + 我的用户名 + 關於我 + 頭像鏈接 + 橫幅鏈接 + 網站鏈接 + LN地址 + LN鏈接(過期) + 圖片已保存到相冊 + 保存圖片失敗 + 上傳圖片 + 上傳中… + 用户尚未設置閃電地址以接收聰 + "回覆這裏.. " + 複製筆記ID到剪貼板,供分享 + 複製頻道ID(筆記)到剪貼板 + 編輯頻道元數據 + 加入 + 私聊 + 新請求 + 已隱藏用户 + 新動態 + 會話 + 動態 + 回覆 + 關注 + 舉報 + 更多選項 + 中繼 + 網站 + 閃電地址 + 複製 Nsec ID(您的私人密鑰)到剪貼板以備備份 + 複製私鑰到剪貼板 + 複製公鑰到剪貼板以供共享 + 複製公鑰(NPub)到剪貼板 + 發送直接消息 + 編輯用户元數據 + 關注 + 不隱藏 + 複製用户ID + 隱藏該用户 + npub,hex,用户名 + 清除 + 應用標誌 + nsec/ npub/ hex 私鑰 + 顯示密碼 + 隱藏密碼 + 無效密鑰 + 我接受 + 使用條款 + 需要接受條款 + 需要密鑰 + 登錄 + 生成新密鑰 + 正在加載 + 加載回覆出錯: + 重試 + 信息流為空。 + 刷新 + 創建 + 描述為 + 並上傳圖片 + 將聊天名稱修改為 + 描述更改為 + 圖片更改為 + 離開 + 取關 + 頻道已創建 + 頻道信息已更改為 + + 公共聊天 + 收到文章 + 移除 + 自動 + + 更改為 + 先顯示 + 在前 + 總是翻譯為 + 從不 + 現在 + 小時 + + + 裸體 + 不文雅的言辭 / 敵意言論 + 舉報敵意言論 + 舉報裸體 / 情色內容 + 其他 + 將所有已知內容標記為已讀 + 將所有新內容標記為已讀 + 將所有內容標記為已讀 + + ## 備份與安全提示 + \n\n您的帳户由一個私人密鑰保護。 密鑰是以**nsec1**開頭的長隨機字符串。任何擁有您的私人密鑰的人都可以使用您的身份發佈內容。 + \n\n- **不要**將您的私人密鑰添加到您不信任的任何網站或軟件,亦不要在網上公開。 + \n- Amethyst 開發人員**永遠不會**要求您提供私人密鑰。 + \n- **請**保留您的私人密鑰的安全備份,以備帳户恢復。 我們建議使用密碼管理器。 + + 私人密鑰(nsec)已複製到剪貼板 + 複製我的私人密鑰 + 身份驗證失敗 + 錯誤 + "由 %1$s 創建" + "頒發給 %1$s 的徽章圖片" + 您收到了新的徽章獎勵 + 徽章獎勵授予 + 文本已複製到剪貼板 + 複製作者的 @npub 到剪貼板 + 複製文章ID (@note1) 到剪貼板 + 選擇文本 + 選擇 + 分享瀏覽網址 + 分享 + @提及 + 引用 + 複製 + 刪除 + 取關 + 關注 + 請求刪貼 + Amethyst 將請求您的記錄從當前連接的中繼器中刪除。不能保證您發佈的筆記將被永久從那些中繼器或其他存儲筆記的中繼器中刪除。 + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 000000000..1ea0911ad --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,206 @@ + + 對準 QR + 顯示 QR + 頭像圖片 + 掃描 QR + 一直顯示 + 貼文被標記為不當 + 未找到貼文 + 頻道圖片 + 未找到相關事件 + 無法解密消息 + 群組圖片 + 露骨內容 + 垃圾郵件 + 冒充 + 非法行為 + 未知 + 中繼器圖標 + 未知作者 + 複製文本 + 複製作者@npub + 複製筆記ID + 廣播 + + 舉報垃圾郵件/詐騙 + 舉報冒充 + 舉報露骨內容 + 舉報非法行為 + 使用私鑰登錄以便回覆 + 使用私鑰登錄以便提升貼文 + 使用私鑰登錄以便點贊貼文 + 沒有設置Zap金額。長按以更改 + 使用私鑰登錄以便發送Zaps + Zaps + 瀏覽次數 + 提升 + 已提升 + 引用 + 新的金額 (sats) + 添加 + "回覆 " + " 和 " + " 在頻道 " + 個人檔案橫幅 + " 關注" + " 粉絲" + 個人檔案 + 安全過濾器 + 登出 + 顯示更多 + 閃電網絡發票 + 付款 + 閃電小費 + 寫給收款方的留言 + 非常感謝! + 數量 (sats) + 發送 sats + "不再翻譯" + "解析 %1$s 預覽出錯:%2$s" + "預覽 %1$s 卡片圖片" + 新頻道 + 頻道名 + 我的精彩羣聊 + 圖片鏈接 + 描述 + "關於我們.. " + 你在想什麼? + 發佈 + 保存 + 創建 + 取消 + 上傳圖片失敗 + 中繼器地址 + 貼文 + 錯誤 + 主頁 + 私信 + 公開聊天 + 全球 + 搜索 + 添加中繼器 + 顯示名稱 + 我的顯示名稱 + 用户名 + 我的用户名 + 關於我 + 頭像鏈接 + 橫幅鏈接 + 網站鏈接 + LN 地址 + LN 鏈接(過期) + 圖片已保存到相冊 + 保存圖片失敗 + 上傳圖片 + 上傳中… + 用户尚未設置閃電地址以接收 sats + "回覆這裡.." + 複製筆記ID到剪貼板,供分享 + 複製頻道ID(筆記)到剪貼板 + 編輯頻道元數據 + 加入 + 已准 + 新請求 + 已隱藏用户 + 新動態 + 會話 + 動態 + 回覆 + 關注 + 舉報 + 更多選項 + 中繼 + 網站 + 閃電地址 + 複製 NSec (私人密鑰)到剪貼板以備備份 + 複製私鑰到剪貼板 + 複製公鑰到剪貼板以供共享 + 複製公鑰(NPub)到剪貼板 + 發送直接消息 + 編輯用户元數據 + 關注 + 不隱藏 + 複製用户ID + 隱藏該用户 + npub,hex,用户名 + 清除 + 應用標誌 + nsec/ npub/ hex 私鑰 + 顯示密碼 + 隱藏密碼 + 無效密鑰 + 我接受 + 使用條款 + 需要接受條款 + 需要密鑰 + 登錄 + 生成新密鑰 + 正在加載 + 加載回覆出錯: + 重試 + 信息流為空。 + 刷新 + 創建 + 描述為 + 並上傳圖片 + 將聊天名稱修改為 + 描述更改為 + 圖片更改為 + 離開 + 取關 + 頻道已創建 + 頻道信息已更改為 + + 公共聊天 + 收到文章 + 移除 + 自動 + + 更改為 + 先顯示 + 在前 + 總是翻譯為 + 從不 + 現在 + 小時 + + + 裸體 + 不文雅的言辭 / 敵意言論 + 舉報敵意言論 + 舉報裸體 / 情色內容 + 其他 + 將所有已知內容標記為已讀 + 將所有新內容標記為已讀 + 將所有內容標記為已讀 + + ## 備份與安全提示 + \n\n您的帳户由一個私人密鑰保護。 密鑰是以**nsec1**開頭的長隨機字符串。任何擁有您的私人密鑰的人都可以使用您的身份發佈內容。 + \n\n- **不要**將您的私人密鑰添加到您不信任的任何網站或軟件,亦不要在網上公開。 + \n- Amethyst 開發人員**永遠不會**要求您提供私人密鑰。 + \n- **請**保留您的私人密鑰的安全備份,以備帳户恢復。 我們建議使用密碼管理器。 + + 私人密鑰(nsec)已複製到剪貼板 + 複製我的私人密鑰 + 身份驗證失敗 + 錯誤 + "由 %1$s 創建" + "頒發給 %1$s 的徽章圖片" + 您收到了新的徽章獎勵 + 徽章獎勵授予 + 文本已複製到剪貼板 + 複製作者的 @npub 到剪貼板 + 複製文章ID (@note1) 到剪貼板 + 選擇文本 + 選擇 + 分享瀏覽網址 + 分享 + 作者 ID + 貼文 ID + 複製 + 刪除 + 取關 + 關注 + 請求刪貼 + Amethyst 將請求您的記錄從當前連接的中繼器中刪除。不能保證您發佈的筆記將被永久從那些中繼器或其他存儲筆記的中繼器中刪除。 + \ No newline at end of file diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml new file mode 100644 index 000000000..4a8945e5f --- /dev/null +++ b/app/src/main/res/values-zh/strings.xml @@ -0,0 +1,205 @@ + + 对准二维码 + 显示二维码 + 头像图片 + 扫描二维码 + 一直显示 + 帖文被标记为不当 + 未找到帖子 + 频道图片 + 未找到相关事件 + 无法解密消息 + 群聊图片 + 明确内容 + 垃圾邮件 + 冒充 + 非法行为 + 未知 + 中继器图标 + 未知作者 + 复制文本 + 复制作者@npub + 复制笔记ID + 广播 + + 举报垃圾邮件/诈骗 + 举报冒充 + 举报明确内容 + 举报非法行为 + 使用私钥登录以便回复 + 使用私钥登录以便提升帖子 + 使用私钥登录以便点赞帖子 + 没有设置Zap金额。长按以更改 + 使用私钥登录以便发送Zaps + Zaps + 浏览次数 + 提升 + 已提升 + 引用 + 新的聪金额 + 添加 + "回复 " + " 和 " + " 在频道 " + 个人档案横幅 + " 关注" + " 粉丝" + 个人档案 + 安全过滤器 + 登出 + 显示更多 + 闪电网络发票 + 付款 + 闪电小费 + 聪给收款方的留言 + 非常感谢! + 聪数量 + 发送聪 + "不再翻译" + "解析 %1$s 预览出错:%2$s" + "预览 %1$s 卡片图片" + 新频道 + 频道名 + 我的精彩群聊 + 图片链接 + 描述 + "关于我们.. " + 你在想什么? + 发布 + 保存 + 创建 + 取消 + 上传图片失败 + 中继器地址 + 帖文 + 错误 + 主页 + 私信 + 公开聊天 + 全球 + 搜索 + 添加中继器 + 显示名称 + 我的显示名称 + 用户名 + 我的用户名 + 关于我 + 头像链接 + 横幅链接 + 网站链接 + LN地址 + LN链接(过期) + 图片已保存到相册 + 保存图片失败 + 上传图片 + 上传中… + 用户尚未设置闪电地址以接收聪 + "回复这里.. " + 复制笔记ID到剪贴板,供分享 + 复制频道ID(笔记)到剪贴板 + 编辑频道元数据 + 加入 + 私聊 + 新请求 + 已隐藏用户 + 新动态 + 会话 + 动态 + 回复 + 关注 + 举报 + 更多选项 + 中继 + 网站 + 闪电地址 + 复制 Nsec ID(您的私人密钥)到剪贴板以备备份 + 复制私钥到剪贴板 + 复制公钥到剪贴板以供共享 + 复制公钥(NPub)到剪贴板 + 发送直接消息 + 编辑用户元数据 + 关注 + 不隐藏 + 复制用户ID + 隐藏该用户 + npub,hex,用户名 + 清除 + 应用标志 + nsec/ npub/ hex 私钥 + 显示密码 + 隐藏密码 + 无效密钥 + 我接受 + 使用条款 + 需要接受条款 + 需要密钥 + 登录 + 生成新密钥 + 正在加载 + 加载回复出错: + 重试 + 信息流为空。 + 刷新 + 创建 + 描述为 + 并上传图片 + 将聊天名称修改为 + 描述更改为 + 图片更改为 + 离开 + 取关 + 频道已创建 + 频道信息已更改为 + 公共聊天 + 收到文章 + 移除 + 自动 + + 更改为 + 先显示 + 在前 + 总是翻译为 + 从不 + 现在 + 小时 + + + 裸体 + 不文雅的言辞 / 敌意言论 + 举报敌意言论 + 举报裸体 / 情色内容 + 其他 + 将所有已知内容标记为已读 + 将所有新内容标记为已读 + 将所有内容标记为已读 + + ## 备份与安全提示 + \n\n您的帐户由一个私人密钥保护。 密钥是以**nsec1**开头的长随机字符串。任何拥有您的私人密钥的人都可以使用您的身份发布内容。 + \n\n- **不要**将您的私人密钥添加到您不信任的任何网站或软件,亦不要在网上公开。 + \n- Amethyst 开发人员**永远不会**要求您提供私人密钥。 + \n- **请**保留您的私人密钥的安全备份,以备帐户恢复。 我们建议使用密码管理器。 + + 私人密钥(nsec)已复制到剪贴板 + 复制我的私人密钥 + 身份验证失败 + 错误 + "由 %1$s 创建" + "颁发给 %1$s 的徽章图片" + 您收到了新的徽章奖励 + 徽章奖励授予 + 文本已复制到剪贴板 + 复制作者的 @npub 到剪贴板 + 复制文章ID (@note1) 到剪贴板 + 选择文本 + 选择 + 分享浏览网址 + 分享 + @提及 + 引用 + 复制 + 删除 + 取关 + 关注 + 请求删贴 + Amethyst 将请求您的记录从当前连接的中继器中删除。不能保证您发布的笔记将被永久从那些中继器或其他存储笔记的中继器中删除。 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a570d3326..f3c2a9d95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,7 +20,7 @@ Relay Icon Unknown Author Copy Text - Copy Author @npub + Copy Author ID Copy Note ID Broadcast @@ -200,12 +200,25 @@ Select Share Browser Link Share - Copy author’s ID - Copy note ID - Copy + Author ID + Note ID + Copy Text Delete Unfollow 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> + + +