Merge branch 'main' into main

This commit is contained in:
Vitor Pamplona
2023-04-03 20:26:50 -04:00
committed by GitHub
17 changed files with 194 additions and 84 deletions
+2 -2
View File
@@ -12,8 +12,8 @@ android {
applicationId "com.vitorpamplona.amethyst" applicationId "com.vitorpamplona.amethyst"
minSdk 26 minSdk 26
targetSdk 33 targetSdk 33
versionCode 113 versionCode 114
versionName "0.31.3" versionName "0.31.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables { vectorDrawables {
@@ -4,6 +4,7 @@ import android.util.Log
import android.util.LruCache import android.util.LruCache
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.vitorpamplona.amethyst.service.model.Event import com.vitorpamplona.amethyst.service.model.Event
import com.vitorpamplona.amethyst.service.relays.Relay
import com.vitorpamplona.amethyst.ui.components.BundledUpdate import com.vitorpamplona.amethyst.ui.components.BundledUpdate
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -14,7 +15,7 @@ class AntiSpamFilter {
val spamMessages = LruCache<Int, Spammer>(1000) val spamMessages = LruCache<Int, Spammer>(1000)
@Synchronized @Synchronized
fun isSpam(event: Event): Boolean { fun isSpam(event: Event, relay: Relay?): Boolean {
val idHex = event.id val idHex = event.id
// if short message, ok // if short message, ok
@@ -27,7 +28,7 @@ class AntiSpamFilter {
val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode() val hash = (event.content + event.tags.flatten().joinToString(",")).hashCode()
if ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null) { if ((recentMessages[hash] != null && recentMessages[hash] != idHex) || spamMessages[hash] != null) {
Log.w("Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${event.content.replace("\n", " | ")}") Log.w("Potential SPAM Message", "${event.id} ${recentMessages[hash]} ${spamMessages[hash] != null} ${relay?.url} ${event.content.replace("\n", " | ")}")
// Log down offenders // Log down offenders
if (spamMessages.get(hash) == null) { if (spamMessages.get(hash) == null) {
@@ -38,14 +38,14 @@ fun HexKey.toDisplayHexKey(): String {
} }
fun decodePublicKey(key: String): ByteArray { fun decodePublicKey(key: String): ByteArray {
val parsed = Nip19.uriToRoute(key)
val pubKeyParsed = parsed?.hex?.toByteArray()
return if (key.startsWith("nsec")) { return if (key.startsWith("nsec")) {
Persona(privKey = key.bechToBytes()).pubKey Persona(privKey = key.bechToBytes()).pubKey
} else if (key.startsWith("npub")) { } else if (pubKeyParsed != null) {
key.bechToBytes() pubKeyParsed
} else if (key.startsWith("note")) { } else {
key.bechToBytes()
} else { // if (pattern.matcher(key).matches()) {
// } else {
Hex.decode(key) Hex.decode(key)
} }
} }
@@ -166,7 +166,7 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
if (antiSpam.isSpam(event)) { if (antiSpam.isSpam(event, relay)) {
relay?.let { relay?.let {
it.spamCounter++ it.spamCounter++
} }
@@ -202,7 +202,7 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event?.id() == event.id()) return if (note.event?.id() == event.id()) return
if (antiSpam.isSpam(event)) { if (antiSpam.isSpam(event, relay)) {
relay?.let { relay?.let {
it.spamCounter++ it.spamCounter++
} }
@@ -317,12 +317,10 @@ object LocalCache {
fun consume(event: ContactListEvent) { fun consume(event: ContactListEvent) {
val user = getOrCreateUser(event.pubKey) val user = getOrCreateUser(event.pubKey)
val follows = event.unverifiedFollowKeySet()
if (event.createdAt > (user.latestContactList?.createdAt ?: 0) && !follows.isNullOrEmpty()) { // avoids processing empty contact lists.
// Saves relay list only if it's a user that is currently been seen if (event.createdAt > (user.latestContactList?.createdAt ?: 0) && !event.tags.isEmpty()) {
user.updateContactList(event) user.updateContactList(event)
// Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}") // Log.d("CL", "AAA ${user.toBestDisplayName()} ${follows.size}")
} }
} }
@@ -558,7 +556,7 @@ object LocalCache {
// Already processed this event. // Already processed this event.
if (note.event != null) return if (note.event != null) return
if (antiSpam.isSpam(event)) { if (antiSpam.isSpam(event, relay)) {
relay?.let { relay?.let {
it.spamCounter++ it.spamCounter++
} }
@@ -155,6 +155,7 @@ open class Note(val idHex: String) {
} }
} }
@Synchronized
fun addZap(zapRequest: Note, zap: Note?) { fun addZap(zapRequest: Note, zap: Note?) {
if (zapRequest !in zaps.keys) { if (zapRequest !in zaps.keys) {
zaps = zaps + Pair(zapRequest, zap) zaps = zaps + Pair(zapRequest, zap)
@@ -273,7 +273,7 @@ class User(val pubkeyHex: String) {
} }
fun transientFollowerCount(): Int { fun transientFollowerCount(): Int {
return LocalCache.users.values.count { it.latestContactList?.let { pubkeyHex in it.unverifiedFollowKeySet() } ?: false } return LocalCache.users.values.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
} }
fun cachedFollowingKeySet(): Set<HexKey> { fun cachedFollowingKeySet(): Set<HexKey> {
@@ -289,7 +289,7 @@ class User(val pubkeyHex: String) {
} }
fun cachedFollowerCount(): Int { fun cachedFollowerCount(): Int {
return LocalCache.users.values.count { it.latestContactList?.let { pubkeyHex in it.unverifiedFollowKeySet() } ?: false } return LocalCache.users.values.count { it.latestContactList?.isTaggedUser(pubkeyHex) ?: false }
} }
fun hasSentMessagesTo(user: User?): Boolean { fun hasSentMessagesTo(user: User?): Boolean {
@@ -40,9 +40,13 @@ class LnZapEvent(
null null
} }
} }
override fun message(): String {
return message
}
val message = content
override fun containedPost(): Event? = try { override fun containedPost(): Event? = try {
description()?.let { description()?.ifBlank { null }?.let {
fromJson(it, Client.lenient) fromJson(it, Client.lenient)
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -63,4 +67,4 @@ class LnZapEvent(
companion object { companion object {
const val kind = 9735 const val kind = 9735
} }
} }
@@ -15,4 +15,6 @@ interface LnZapEventInterface : EventInterface {
fun amount(): BigDecimal? fun amount(): BigDecimal?
fun containedPost(): Event? fun containedPost(): Event?
fun message(): String
} }
@@ -2,7 +2,11 @@ package com.vitorpamplona.amethyst.service.relays
import com.vitorpamplona.amethyst.model.User import com.vitorpamplona.amethyst.model.User
class EOSETime(var time: Long) class EOSETime(var time: Long) {
override fun toString(): String {
return time.toString()
}
}
class EOSERelayList(var relayList: Map<String, EOSETime> = emptyMap()) { class EOSERelayList(var relayList: Map<String, EOSETime> = emptyMap()) {
fun addOrUpdate(relayUrl: String, time: Long) { fun addOrUpdate(relayUrl: String, time: Long) {
@@ -167,7 +167,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
if (isValidURL(myUrlPreview)) { if (isValidURL(myUrlPreview)) {
val removedParamsFromUrl = val removedParamsFromUrl =
myUrlPreview.split("?")[0].lowercase() myUrlPreview.split("?")[0].lowercase()
if (imageExtensions.any { removedParamsFromUrl.endsWith(it, true) }) { if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
AsyncImage( AsyncImage(
model = myUrlPreview, model = myUrlPreview,
contentDescription = myUrlPreview, contentDescription = myUrlPreview,
@@ -182,7 +182,7 @@ fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = n
RoundedCornerShape(15.dp) RoundedCornerShape(15.dp)
) )
) )
} else if (videoExtensions.any { removedParamsFromUrl.endsWith(it, true) }) { } else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
VideoView(myUrlPreview) VideoView(myUrlPreview)
} else { } else {
UrlPreview(myUrlPreview, myUrlPreview) UrlPreview(myUrlPreview, myUrlPreview)
@@ -15,7 +15,6 @@ import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@@ -138,10 +137,10 @@ fun RichTextViewer(
// sequence of images will render in a slideview // sequence of images will render in a slideview
if (isValidURL(word)) { if (isValidURL(word)) {
val removedParamsFromUrl = word.split("?")[0].lowercase() val removedParamsFromUrl = word.split("?")[0].lowercase()
if (imageExtensions.any { word.endsWith(it, true) }) { if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
imagesForPager.add(word) imagesForPager.add(word)
} }
if (videoExtensions.any { word.endsWith(it, true) }) { if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
imagesForPager.add(word) imagesForPager.add(word)
} }
} }
@@ -155,28 +154,59 @@ fun RichTextViewer(
s.forEach { word: String -> s.forEach { word: String ->
if (canPreview) { if (canPreview) {
// Explicit URL // Explicit URL
val lnInvoice = LnInvoiceUtil.findInvoice(word)
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
if (isValidURL(word)) { if (isValidURL(word)) {
val removedParamsFromUrl = word.split("?")[0].lowercase() val removedParamsFromUrl = word.split("?")[0].lowercase()
if (imageExtensions.any { word.endsWith(it, true) }) { if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
ZoomableImageView(word, imagesForPager) ZoomableImageView(word, imagesForPager)
} else if (videoExtensions.any { word.endsWith(it, true) }) { } else if (videoExtensions.any { removedParamsFromUrl.endsWith(it) }) {
ZoomableImageView(word, imagesForPager) ZoomableImageView(word, imagesForPager)
} else { } else {
UrlPreview(word, "$word ") UrlPreview(word, "$word ")
} }
} else if (lnInvoice != null) { } else if (word.startsWith("lnbc", true)) {
InvoicePreview(lnInvoice) val lnInvoice = LnInvoiceUtil.findInvoice(word)
} else if (lnWithdrawal != null) { if (lnInvoice != null) {
ClickableWithdrawal(withdrawalString = lnWithdrawal) InvoicePreview(lnInvoice)
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else if (word.startsWith("lnurl", true)) {
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
if (lnWithdrawal != null) {
ClickableWithdrawal(withdrawalString = lnWithdrawal)
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { } else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
ClickableEmail(word) ClickableEmail(word)
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) { } else if (word.length > 6 && Patterns.PHONE.matcher(word).matches()) {
ClickablePhone(word) ClickablePhone(word)
} else if (isBechLink(word)) { } else if (isBechLink(word)) {
BechLink(word, navController) BechLink(word, navController)
} else if (word.startsWith("#")) {
if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(
word,
tags,
canPreview,
backgroundColor,
accountViewModel,
navController
)
} else if (hashTagsPattern.matcher(word).matches()) {
HashTag(word, accountViewModel, navController)
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else if (noProtocolUrlValidator.matcher(word).matches()) { } else if (noProtocolUrlValidator.matcher(word).matches()) {
val matcher = noProtocolUrlValidator.matcher(word) val matcher = noProtocolUrlValidator.matcher(word)
matcher.find() matcher.find()
@@ -185,10 +215,6 @@ fun RichTextViewer(
ClickableUrl(url, "https://$url") ClickableUrl(url, "https://$url")
Text("$additionalChars ") Text("$additionalChars ")
} else if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
} else if (hashTagsPattern.matcher(word).matches()) {
HashTag(word, accountViewModel, navController)
} else { } else {
Text( Text(
text = "$word ", text = "$word ",
@@ -198,12 +224,40 @@ fun RichTextViewer(
} else { } else {
if (isValidURL(word)) { if (isValidURL(word)) {
ClickableUrl("$word ", word) ClickableUrl("$word ", word)
} else if (word.startsWith("lnurl", true)) {
val lnWithdrawal = LnWithdrawalUtil.findWithdrawal(word)
if (lnWithdrawal != null) {
ClickableWithdrawal(withdrawalString = lnWithdrawal)
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) { } else if (Patterns.EMAIL_ADDRESS.matcher(word).matches()) {
ClickableEmail(word) ClickableEmail(word)
} else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) { } else if (Patterns.PHONE.matcher(word).matches() && word.length > 6) {
ClickablePhone(word) ClickablePhone(word)
} else if (isBechLink(word)) { } else if (isBechLink(word)) {
BechLink(word, navController) BechLink(word, navController)
} else if (word.startsWith("#")) {
if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(
word,
tags,
canPreview,
backgroundColor,
accountViewModel,
navController
)
} else if (hashTagsPattern.matcher(word).matches()) {
HashTag(word, accountViewModel, navController)
} else {
Text(
text = "$word ",
style = LocalTextStyle.current.copy(textDirection = TextDirection.Content)
)
}
} else if (noProtocolUrlValidator.matcher(word).matches()) { } else if (noProtocolUrlValidator.matcher(word).matches()) {
val matcher = noProtocolUrlValidator.matcher(word) val matcher = noProtocolUrlValidator.matcher(word)
matcher.find() matcher.find()
@@ -212,10 +266,6 @@ fun RichTextViewer(
ClickableUrl(url, "https://$url") ClickableUrl(url, "https://$url")
Text("$additionalChars ") Text("$additionalChars ")
} else if (tagIndex.matcher(word).matches() && tags != null) {
TagLink(word, tags, canPreview, backgroundColor, accountViewModel, navController)
} else if (hashTagsPattern.matcher(word).matches()) {
HashTag(word, accountViewModel, navController)
} else { } else {
Text( Text(
text = "$word ", text = "$word ",
@@ -235,9 +285,9 @@ private fun isArabic(text: String): Boolean {
} }
fun isBechLink(word: String): Boolean { fun isBechLink(word: String): Boolean {
val cleaned = word.removePrefix("@").removePrefix("nostr:").removePrefix("@") val cleaned = word.removePrefix("@").removePrefix("nostr:").removePrefix("@").take(7).lowercase()
return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it, true) } return listOf("npub1", "naddr1", "note1", "nprofile1", "nevent1").any { cleaned.startsWith(it) }
} }
@Composable @Composable
@@ -340,14 +390,8 @@ fun TagLink(word: String, tags: List<List<String>>, canPreview: Boolean, backgro
if (tags[index][0] == "p") { if (tags[index][0] == "p") {
val baseUser = LocalCache.checkGetOrCreateUser(tags[index][1]) val baseUser = LocalCache.checkGetOrCreateUser(tags[index][1])
if (baseUser != null) { if (baseUser != null) {
val userState = baseUser.live().metadata.observeAsState() ClickableUserTag(baseUser, navController)
val user = userState.value?.user Text(text = "$extraCharacters ")
if (user != null) {
ClickableUserTag(user, navController)
Text(text = "$extraCharacters ")
} else {
Text(text = "$word ")
}
} else { } else {
// if here the tag is not a valid Nostr Hex // if here the tag is not a valid Nostr Hex
Text(text = "$word ") Text(text = "$word ")
@@ -62,7 +62,8 @@ fun ZoomableImageView(word: String, images: List<String> = listOf(word)) {
mutableStateOf<AsyncImagePainter.State?>(null) mutableStateOf<AsyncImagePainter.State?>(null)
} }
if (imageExtensions.any { word.endsWith(it, true) }) { val removedParamsFromUrl = word.split("?")[0].lowercase()
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
AsyncImage( AsyncImage(
model = word, model = word,
contentDescription = word, contentDescription = word,
@@ -171,7 +172,8 @@ fun ZoomableImageDialog(imageUrl: String, allImages: List<String> = listOf(image
@Composable @Composable
private fun RenderImageOrVideo(imageUrl: String) { private fun RenderImageOrVideo(imageUrl: String) {
if (imageExtensions.any { imageUrl.endsWith(it, true) }) { val removedParamsFromUrl = imageUrl.split("?")[0].lowercase()
if (imageExtensions.any { removedParamsFromUrl.endsWith(it) }) {
AsyncImage( AsyncImage(
model = imageUrl, model = imageUrl,
contentDescription = stringResource(id = R.string.profile_image), contentDescription = stringResource(id = R.string.profile_image),
@@ -96,7 +96,7 @@ fun NoteCompose(
) )
} }
Log.d("Time", "Note Compose in $elapsed for ${baseNote.event?.content()?.split("\n")?.get(0)?.take(100)}") Log.d("Time", "Note Compose in $elapsed for ${baseNote.event?.kind()} ${baseNote.event?.content()?.split("\n")?.get(0)?.take(100)}")
} }
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@@ -66,7 +66,7 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(horizontal = 10.dp), .padding(horizontal = 10.dp),
verticalArrangement = Arrangement.SpaceBetween verticalArrangement = Arrangement.SpaceAround
) { ) {
if (presenting) { if (presenting) {
Column(modifier = Modifier.fillMaxWidth()) { Column(modifier = Modifier.fillMaxWidth()) {
@@ -91,22 +91,22 @@ fun ShowQRDialog(user: User, onScan: (String) -> Unit, onClose: () -> Unit) {
fontSize = 18.sp fontSize = 18.sp
) )
} }
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 35.dp, vertical = 10.dp)
) {
QrCodeDrawer("nostr:${user.pubkeyNpub()}")
}
} }
Row( Row(
horizontalArrangement = Arrangement.Center, horizontalArrangement = Arrangement.Center,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 30.dp, vertical = 10.dp) .padding(horizontal = 35.dp)
) {
QrCodeDrawer("nostr:${user.pubkeyNpub()}")
}
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 30.dp)
) { ) {
Button( Button(
onClick = { presenting = false }, onClick = { presenting = false },
@@ -4,6 +4,8 @@ import androidx.lifecycle.ViewModel
import com.vitorpamplona.amethyst.LocalPreferences import com.vitorpamplona.amethyst.LocalPreferences
import com.vitorpamplona.amethyst.ServiceManager import com.vitorpamplona.amethyst.ServiceManager
import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.toByteArray
import com.vitorpamplona.amethyst.service.nip19.Nip19
import fr.acinq.secp256k1.Hex import fr.acinq.secp256k1.Hex
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
@@ -39,12 +41,14 @@ class AccountStateViewModel() : ViewModel() {
fun login(key: String) { fun login(key: String) {
val pattern = Pattern.compile(".+@.+\\.[a-z]+") val pattern = Pattern.compile(".+@.+\\.[a-z]+")
val parsed = Nip19.uriToRoute(key)
val pubKeyParsed = parsed?.hex?.toByteArray()
val account = val account =
if (key.startsWith("nsec")) { if (key.startsWith("nsec")) {
Account(Persona(privKey = key.bechToBytes())) Account(Persona(privKey = key.bechToBytes()))
} else if (key.startsWith("npub")) { } else if (pubKeyParsed != null) {
Account(Persona(pubKey = key.bechToBytes())) Account(Persona(pubKey = pubKeyParsed))
} else if (pattern.matcher(key).matches()) { } else if (pattern.matcher(key).matches()) {
// Evaluate NIP-5 // Evaluate NIP-5
Account(Persona()) Account(Persona())
@@ -73,6 +73,7 @@ import com.vitorpamplona.amethyst.ui.dal.UserProfileFollowsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileNewThreadFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileReportsFeedFilter
import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter import com.vitorpamplona.amethyst.ui.dal.UserProfileZapsFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.ShowQRDialog
import com.vitorpamplona.amethyst.ui.note.UserPicture import com.vitorpamplona.amethyst.ui.note.UserPicture
import com.vitorpamplona.amethyst.ui.note.showAmount import com.vitorpamplona.amethyst.ui.note.showAmount
import com.vitorpamplona.amethyst.ui.screen.FeedView import com.vitorpamplona.amethyst.ui.screen.FeedView
@@ -450,16 +451,41 @@ private fun DrawAdditionalInfo(baseUser: User, account: Account, accountViewMode
IconButton( IconButton(
modifier = Modifier modifier = Modifier
.size(30.dp) .size(25.dp)
.padding(start = 5.dp), .padding(start = 5.dp),
onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); } onClick = { clipboardManager.setText(AnnotatedString(user.pubkeyNpub())); }
) { ) {
Icon( Icon(
imageVector = Icons.Default.ContentCopy, imageVector = Icons.Default.ContentCopy,
null, null,
modifier = Modifier modifier = Modifier.size(15.dp),
.padding(end = 5.dp) tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
.size(15.dp), )
}
var dialogOpen by remember {
mutableStateOf(false)
}
if (dialogOpen) {
ShowQRDialog(
user,
onScan = {
dialogOpen = false
navController.navigate(it)
},
onClose = { dialogOpen = false }
)
}
IconButton(
modifier = Modifier.size(25.dp),
onClick = { dialogOpen = true }
) {
Icon(
painter = painterResource(R.drawable.ic_qrcode),
null,
modifier = Modifier.size(15.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f) tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
) )
} }
@@ -36,6 +36,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
import java.util.* import java.util.*
@@ -51,6 +52,9 @@ fun LoginPage(
var termsAcceptanceIsRequired by remember { mutableStateOf("") } var termsAcceptanceIsRequired by remember { mutableStateOf("") }
val uri = LocalUriHandler.current val uri = LocalUriHandler.current
val context = LocalContext.current val context = LocalContext.current
var dialogOpen by remember {
mutableStateOf(false)
}
Column( Column(
modifier = Modifier modifier = Modifier
@@ -117,16 +121,36 @@ fun LoginPage(
) )
}, },
trailingIcon = { trailingIcon = {
IconButton(onClick = { showPassword = !showPassword }) { Row {
Icon( IconButton(onClick = { showPassword = !showPassword }) {
imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility, Icon(
contentDescription = if (showPassword) { imageVector = if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
stringResource(R.string.show_password) contentDescription = if (showPassword) {
} else { stringResource(R.string.show_password)
stringResource( } else {
R.string.hide_password stringResource(
) R.string.hide_password
)
}
)
}
}
},
leadingIcon = {
if (dialogOpen) {
SimpleQrCodeScanner {
dialogOpen = false
if (!it.isNullOrEmpty()) {
key.value = TextFieldValue(it)
} }
}
}
IconButton(onClick = { dialogOpen = true }) {
Icon(
painter = painterResource(R.drawable.ic_qrcode),
null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.primary
) )
} }
}, },