mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-06-05 08:49:16 +02:00
New Signup screen
This commit is contained in:
parent
d27b9ae5a8
commit
7b7e3624ac
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,6 +9,7 @@
|
|||||||
/.idea/deploymentTargetDropDown.xml
|
/.idea/deploymentTargetDropDown.xml
|
||||||
/.idea/appInsightsSettings.xml
|
/.idea/appInsightsSettings.xml
|
||||||
/.idea/ktlint-plugin.xml
|
/.idea/ktlint-plugin.xml
|
||||||
|
/.idea/ktfmt.xml
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
|
@ -130,8 +130,11 @@ object LocalPreferences {
|
|||||||
return currentAccount
|
return currentAccount
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateCurrentAccount(npub: String) {
|
private fun updateCurrentAccount(npub: String?) {
|
||||||
if (currentAccount != npub) {
|
if (npub == null) {
|
||||||
|
currentAccount = null
|
||||||
|
encryptedPreferences().edit().clear().apply()
|
||||||
|
} else if (currentAccount != npub) {
|
||||||
currentAccount = npub
|
currentAccount = npub
|
||||||
|
|
||||||
encryptedPreferences().edit().apply { putString(PrefKeys.CURRENT_ACCOUNT, npub) }.apply()
|
encryptedPreferences().edit().apply { putString(PrefKeys.CURRENT_ACCOUNT, npub) }.apply()
|
||||||
@ -250,7 +253,7 @@ object LocalPreferences {
|
|||||||
deleteUserPreferenceFile(accountInfo.npub)
|
deleteUserPreferenceFile(accountInfo.npub)
|
||||||
|
|
||||||
if (savedAccounts().isEmpty()) {
|
if (savedAccounts().isEmpty()) {
|
||||||
encryptedPreferences().edit().clear().apply()
|
updateCurrentAccount(null)
|
||||||
} else if (currentAccount() == accountInfo.npub) {
|
} else if (currentAccount() == accountInfo.npub) {
|
||||||
updateCurrentAccount(savedAccounts().elementAt(0).npub)
|
updateCurrentAccount(savedAccounts().elementAt(0).npub)
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,6 @@ import com.vitorpamplona.quartz.events.GeneralListEvent
|
|||||||
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
import com.vitorpamplona.quartz.events.GenericRepostEvent
|
||||||
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
import com.vitorpamplona.quartz.events.GiftWrapEvent
|
||||||
import com.vitorpamplona.quartz.events.HTTPAuthorizationEvent
|
import com.vitorpamplona.quartz.events.HTTPAuthorizationEvent
|
||||||
import com.vitorpamplona.quartz.events.IdentityClaim
|
|
||||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||||
import com.vitorpamplona.quartz.events.LnZapPaymentRequestEvent
|
import com.vitorpamplona.quartz.events.LnZapPaymentRequestEvent
|
||||||
@ -541,14 +540,36 @@ class Account(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun sendNewUserMetadata(
|
fun sendNewUserMetadata(
|
||||||
toString: String,
|
name: String? = null,
|
||||||
newName: String,
|
picture: String? = null,
|
||||||
identities: List<IdentityClaim>,
|
banner: String? = null,
|
||||||
|
website: String? = null,
|
||||||
|
about: String? = null,
|
||||||
|
nip05: String? = null,
|
||||||
|
lnAddress: String? = null,
|
||||||
|
lnURL: String? = null,
|
||||||
|
twitter: String? = null,
|
||||||
|
mastodon: String? = null,
|
||||||
|
github: String? = null,
|
||||||
) {
|
) {
|
||||||
if (!isWriteable()) return
|
if (!isWriteable()) return
|
||||||
|
|
||||||
MetadataEvent.create(toString, newName, identities, signer) {
|
MetadataEvent.updateFromPast(
|
||||||
|
latest = userProfile().info?.latestMetadata,
|
||||||
|
name = name,
|
||||||
|
picture = picture,
|
||||||
|
banner = banner,
|
||||||
|
website = website,
|
||||||
|
about = about,
|
||||||
|
nip05 = nip05,
|
||||||
|
lnAddress = lnAddress,
|
||||||
|
lnURL = lnURL,
|
||||||
|
twitter = twitter,
|
||||||
|
mastodon = mastodon,
|
||||||
|
github = github,
|
||||||
|
signer = signer,
|
||||||
|
) {
|
||||||
Client.send(it)
|
Client.send(it)
|
||||||
LocalCache.justConsume(it, null)
|
LocalCache.justConsume(it, null)
|
||||||
}
|
}
|
||||||
|
@ -27,8 +27,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper
|
|
||||||
import com.fasterxml.jackson.databind.node.ObjectNode
|
|
||||||
import com.vitorpamplona.amethyst.model.Account
|
import com.vitorpamplona.amethyst.model.Account
|
||||||
import com.vitorpamplona.amethyst.service.Nip96Uploader
|
import com.vitorpamplona.amethyst.service.Nip96Uploader
|
||||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||||
@ -39,8 +37,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.channels.BufferOverflow
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.StringWriter
|
|
||||||
|
|
||||||
class NewUserMetadataViewModel : ViewModel() {
|
class NewUserMetadataViewModel : ViewModel() {
|
||||||
private lateinit var account: Account
|
private lateinit var account: Account
|
||||||
@ -97,59 +93,22 @@ class NewUserMetadataViewModel : ViewModel() {
|
|||||||
|
|
||||||
fun create() {
|
fun create() {
|
||||||
// Tries to not delete any existing attribute that we do not work with.
|
// Tries to not delete any existing attribute that we do not work with.
|
||||||
val latest = account.userProfile().info?.latestMetadata
|
|
||||||
val currentJson =
|
|
||||||
if (latest != null) {
|
|
||||||
ObjectMapper()
|
|
||||||
.readTree(
|
|
||||||
ByteArrayInputStream(latest.content.toByteArray(Charsets.UTF_8)),
|
|
||||||
) as ObjectNode
|
|
||||||
} else {
|
|
||||||
ObjectMapper().createObjectNode()
|
|
||||||
}
|
|
||||||
currentJson.put("name", displayName.value.trim())
|
|
||||||
currentJson.put("display_name", displayName.value.trim())
|
|
||||||
currentJson.put("picture", picture.value.trim())
|
|
||||||
currentJson.put("banner", banner.value.trim())
|
|
||||||
currentJson.put("website", website.value.trim())
|
|
||||||
currentJson.put("about", about.value.trim())
|
|
||||||
currentJson.put("nip05", nip05.value.trim())
|
|
||||||
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)
|
|
||||||
|
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
account.sendNewUserMetadata(writer.buffer.toString(), displayName.value.trim(), newClaims)
|
account.sendNewUserMetadata(
|
||||||
|
name = displayName.value,
|
||||||
|
picture = picture.value,
|
||||||
|
banner = banner.value,
|
||||||
|
website = website.value,
|
||||||
|
about = about.value,
|
||||||
|
nip05 = nip05.value,
|
||||||
|
lnAddress = lnAddress.value,
|
||||||
|
lnURL = lnURL.value,
|
||||||
|
twitter = twitter.value,
|
||||||
|
mastodon = mastodon.value,
|
||||||
|
github = github.value,
|
||||||
|
)
|
||||||
|
clear()
|
||||||
}
|
}
|
||||||
clear()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clear() {
|
fun clear() {
|
||||||
|
@ -73,7 +73,7 @@ import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
|||||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage
|
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen
|
||||||
import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier
|
import com.vitorpamplona.amethyst.ui.theme.AccountPictureModifier
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
import com.vitorpamplona.amethyst.ui.theme.Size10dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||||
@ -121,7 +121,7 @@ fun AccountSwitchBottomSheet(
|
|||||||
) {
|
) {
|
||||||
Surface(modifier = Modifier.fillMaxSize()) {
|
Surface(modifier = Modifier.fillMaxSize()) {
|
||||||
Box {
|
Box {
|
||||||
LoginPage(accountStateViewModel, isFirstLogin = false)
|
LoginOrSignupScreen(accountStateViewModel, isFirstLogin = false)
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(text = stringResource(R.string.account_switch_add_account_dialog_title))
|
Text(text = stringResource(R.string.account_switch_add_account_dialog_title))
|
||||||
|
@ -50,7 +50,7 @@ import com.vitorpamplona.amethyst.ui.MainActivity
|
|||||||
import com.vitorpamplona.amethyst.ui.components.getActivity
|
import com.vitorpamplona.amethyst.ui.components.getActivity
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.MainScreen
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.MainScreen
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginPage
|
import com.vitorpamplona.amethyst.ui.screen.loggedOff.LoginOrSignupScreen
|
||||||
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
import com.vitorpamplona.quartz.signers.NostrSignerExternal
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@ -71,7 +71,7 @@ fun AccountScreen(
|
|||||||
LoadingAccounts()
|
LoadingAccounts()
|
||||||
}
|
}
|
||||||
is AccountState.LoggedOff -> {
|
is AccountState.LoggedOff -> {
|
||||||
LoginPage(accountStateViewModel, isFirstLogin = true)
|
LoginOrSignupScreen(accountStateViewModel, isFirstLogin = true)
|
||||||
}
|
}
|
||||||
is AccountState.LoggedIn -> {
|
is AccountState.LoggedIn -> {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
|
@ -29,6 +29,7 @@ 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.service.HttpClient
|
import com.vitorpamplona.amethyst.service.HttpClient
|
||||||
|
import com.vitorpamplona.amethyst.service.relays.Client
|
||||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||||
import com.vitorpamplona.quartz.encoders.Hex
|
import com.vitorpamplona.quartz.encoders.Hex
|
||||||
import com.vitorpamplona.quartz.encoders.Nip19
|
import com.vitorpamplona.quartz.encoders.Nip19
|
||||||
@ -42,6 +43,7 @@ import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
|||||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.GlobalScope
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
@ -65,8 +67,7 @@ class AccountStateViewModel() : ViewModel() {
|
|||||||
|
|
||||||
private suspend fun tryLoginExistingAccount() =
|
private suspend fun tryLoginExistingAccount() =
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
LocalPreferences.loadCurrentAccountFromEncryptedStorage()?.let { startUI(it) }
|
LocalPreferences.loadCurrentAccountFromEncryptedStorage()?.let { startUI(it) } ?: run { requestLoginUI() }
|
||||||
?: run { requestLoginUI() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun requestLoginUI() {
|
private suspend fun requestLoginUI() {
|
||||||
@ -144,25 +145,33 @@ class AccountStateViewModel() : ViewModel() {
|
|||||||
startUI(account)
|
startUI(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun startUI(account: Account) =
|
suspend fun startUI(
|
||||||
withContext(Dispatchers.Main) {
|
account: Account,
|
||||||
if (account.isWriteable()) {
|
onServicesReady: (() -> Unit)? = null,
|
||||||
_accountContent.update { AccountState.LoggedIn(account) }
|
) = withContext(Dispatchers.Main) {
|
||||||
} else {
|
if (account.isWriteable()) {
|
||||||
_accountContent.update { AccountState.LoggedInViewOnly(account) }
|
_accountContent.update { AccountState.LoggedIn(account) }
|
||||||
}
|
} else {
|
||||||
|
_accountContent.update { AccountState.LoggedInViewOnly(account) }
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
// Prepares livedata objects on the main user.
|
|
||||||
account.userProfile().live()
|
|
||||||
}
|
|
||||||
serviceManager?.restartIfDifferentAccount(account)
|
|
||||||
}
|
|
||||||
|
|
||||||
account.saveable.observeForever(saveListener)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
// Prepares livedata objects on the main user.
|
||||||
|
account.userProfile().live()
|
||||||
|
}
|
||||||
|
serviceManager?.restartIfDifferentAccount(account)
|
||||||
|
|
||||||
|
if (onServicesReady != null) {
|
||||||
|
// waits for the connection to go through
|
||||||
|
delay(1000)
|
||||||
|
onServicesReady()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
account.saveable.observeForever(saveListener)
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class)
|
||||||
private val saveListener: (com.vitorpamplona.amethyst.model.AccountState) -> Unit = {
|
private val saveListener: (com.vitorpamplona.amethyst.model.AccountState) -> Unit = {
|
||||||
GlobalScope.launch(Dispatchers.IO) { LocalPreferences.saveToEncryptedStorage(it.account) }
|
GlobalScope.launch(Dispatchers.IO) { LocalPreferences.saveToEncryptedStorage(it.account) }
|
||||||
@ -204,6 +213,7 @@ class AccountStateViewModel() : ViewModel() {
|
|||||||
fun newKey(
|
fun newKey(
|
||||||
useProxy: Boolean,
|
useProxy: Boolean,
|
||||||
proxyPort: Int,
|
proxyPort: Int,
|
||||||
|
name: String? = null,
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||||
@ -220,7 +230,10 @@ class AccountStateViewModel() : ViewModel() {
|
|||||||
|
|
||||||
// saves to local preferences
|
// saves to local preferences
|
||||||
LocalPreferences.updatePrefsForLogin(account)
|
LocalPreferences.updatePrefsForLogin(account)
|
||||||
startUI(account)
|
startUI(account) {
|
||||||
|
account.userProfile().latestContactList?.let { Client.send(it) }
|
||||||
|
account.sendNewUserMetadata(name = name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2023 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedOff
|
||||||
|
|
||||||
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LoginOrSignupScreen(
|
||||||
|
accountViewModel: AccountStateViewModel,
|
||||||
|
isFirstLogin: Boolean,
|
||||||
|
) {
|
||||||
|
var wantsNewUser by remember {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
Crossfade(wantsNewUser, label = "LoginOrSignupScreen") {
|
||||||
|
if (it) {
|
||||||
|
SignUpPage(accountViewModel = accountViewModel) {
|
||||||
|
wantsNewUser = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LoginPage(accountViewModel = accountViewModel, isFirstLogin = isFirstLogin) {
|
||||||
|
wantsNewUser = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.Column
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@ -75,19 +74,17 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.TextStyle
|
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.vitorpamplona.amethyst.Amethyst
|
import com.vitorpamplona.amethyst.Amethyst
|
||||||
import com.vitorpamplona.amethyst.R
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.service.PackageUtils
|
import com.vitorpamplona.amethyst.service.PackageUtils
|
||||||
@ -96,8 +93,9 @@ import com.vitorpamplona.amethyst.ui.components.getActivity
|
|||||||
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
import com.vitorpamplona.amethyst.ui.qrcode.SimpleQrCodeScanner
|
||||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Font14SP
|
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
||||||
import com.vitorpamplona.quartz.signers.SignerType
|
import com.vitorpamplona.quartz.signers.SignerType
|
||||||
@ -105,11 +103,21 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun LoginPage() {
|
||||||
|
val accountViewModel: AccountStateViewModel = viewModel()
|
||||||
|
|
||||||
|
LoginPage(accountViewModel, true) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class)
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun LoginPage(
|
fun LoginPage(
|
||||||
accountViewModel: AccountStateViewModel,
|
accountViewModel: AccountStateViewModel,
|
||||||
isFirstLogin: Boolean,
|
isFirstLogin: Boolean,
|
||||||
|
onWantsToLogin: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val key = remember { mutableStateOf(TextFieldValue("")) }
|
val key = remember { mutableStateOf(TextFieldValue("")) }
|
||||||
var errorMessage by remember { mutableStateOf("") }
|
var errorMessage by remember { mutableStateOf("") }
|
||||||
@ -208,219 +216,103 @@ fun LoginPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
modifier =
|
||||||
verticalArrangement = Arrangement.SpaceBetween,
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(Size20dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
// The first child is glued to the top.
|
Image(
|
||||||
// Hence we have nothing at the top, an empty box is used.
|
painterResource(id = R.drawable.amethyst),
|
||||||
Box(modifier = Modifier.height(0.dp))
|
contentDescription = stringResource(R.string.app_logo),
|
||||||
|
modifier = Modifier.size(150.dp),
|
||||||
|
contentScale = ContentScale.Inside,
|
||||||
|
)
|
||||||
|
|
||||||
// The second child, this column, is centered vertically.
|
Spacer(modifier = Modifier.height(40.dp))
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(20.dp).fillMaxSize(),
|
var showPassword by remember { mutableStateOf(false) }
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
val autofillNode =
|
||||||
Image(
|
AutofillNode(
|
||||||
painterResource(id = R.drawable.amethyst),
|
autofillTypes = listOf(AutofillType.Password),
|
||||||
contentDescription = stringResource(R.string.app_logo),
|
onFill = { key.value = TextFieldValue(it) },
|
||||||
modifier = Modifier.size(200.dp),
|
|
||||||
contentScale = ContentScale.Inside,
|
|
||||||
)
|
)
|
||||||
|
val autofill = LocalAutofill.current
|
||||||
|
LocalAutofillTree.current += autofillNode
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(40.dp))
|
OutlinedTextField(
|
||||||
|
modifier =
|
||||||
var showPassword by remember { mutableStateOf(false) }
|
Modifier
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
val autofillNode =
|
|
||||||
AutofillNode(
|
|
||||||
autofillTypes = listOf(AutofillType.Password),
|
|
||||||
onFill = { key.value = TextFieldValue(it) },
|
|
||||||
)
|
|
||||||
val autofill = LocalAutofill.current
|
|
||||||
LocalAutofillTree.current += autofillNode
|
|
||||||
|
|
||||||
OutlinedTextField(
|
|
||||||
modifier =
|
|
||||||
Modifier.onGloballyPositioned { coordinates ->
|
|
||||||
autofillNode.boundingBox = coordinates.boundsInWindow()
|
autofillNode.boundingBox = coordinates.boundsInWindow()
|
||||||
}
|
}
|
||||||
.onFocusChanged { focusState ->
|
.onFocusChanged { focusState ->
|
||||||
autofill?.run {
|
autofill?.run {
|
||||||
if (focusState.isFocused) {
|
if (focusState.isFocused) {
|
||||||
requestAutofillForNode(autofillNode)
|
requestAutofillForNode(autofillNode)
|
||||||
} else {
|
} else {
|
||||||
cancelAutofillForNode(autofillNode)
|
cancelAutofillForNode(autofillNode)
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
value = key.value,
|
|
||||||
onValueChange = { key.value = it },
|
|
||||||
keyboardOptions =
|
|
||||||
KeyboardOptions(
|
|
||||||
autoCorrect = false,
|
|
||||||
keyboardType = KeyboardType.Password,
|
|
||||||
imeAction = ImeAction.Go,
|
|
||||||
),
|
|
||||||
placeholder = {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.nsec_npub_hex_private_key),
|
|
||||||
color = MaterialTheme.colorScheme.placeholderText,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailingIcon = {
|
|
||||||
Row {
|
|
||||||
IconButton(onClick = { showPassword = !showPassword }) {
|
|
||||||
Icon(
|
|
||||||
imageVector =
|
|
||||||
if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
|
||||||
contentDescription =
|
|
||||||
if (showPassword) {
|
|
||||||
stringResource(R.string.show_password)
|
|
||||||
} else {
|
|
||||||
stringResource(
|
|
||||||
R.string.hide_password,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
leadingIcon = {
|
|
||||||
if (dialogOpen) {
|
|
||||||
SimpleQrCodeScanner {
|
|
||||||
dialogOpen = false
|
|
||||||
if (!it.isNullOrEmpty()) {
|
|
||||||
key.value = TextFieldValue(it)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
IconButton(onClick = { dialogOpen = true }) {
|
value = key.value,
|
||||||
|
onValueChange = { key.value = it },
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
autoCorrect = false,
|
||||||
|
keyboardType = KeyboardType.Password,
|
||||||
|
imeAction = ImeAction.Go,
|
||||||
|
),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.nsec_npub_hex_private_key),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
Row {
|
||||||
|
IconButton(onClick = { showPassword = !showPassword }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_qrcode),
|
imageVector =
|
||||||
null,
|
if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||||
modifier = Modifier.size(24.dp),
|
contentDescription =
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
if (showPassword) {
|
||||||
|
stringResource(R.string.show_password)
|
||||||
|
} else {
|
||||||
|
stringResource(
|
||||||
|
R.string.hide_password,
|
||||||
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
visualTransformation =
|
},
|
||||||
if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
leadingIcon = {
|
||||||
keyboardActions =
|
if (dialogOpen) {
|
||||||
KeyboardActions(
|
SimpleQrCodeScanner {
|
||||||
onGo = {
|
dialogOpen = false
|
||||||
if (!acceptedTerms.value) {
|
if (!it.isNullOrEmpty()) {
|
||||||
termsAcceptanceIsRequired =
|
key.value = TextFieldValue(it)
|
||||||
context.getString(R.string.acceptance_of_terms_is_required)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key.value.text.isBlank()) {
|
|
||||||
errorMessage = context.getString(R.string.key_is_required)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
|
||||||
accountViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) {
|
|
||||||
errorMessage = context.getString(R.string.invalid_key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
if (errorMessage.isNotBlank()) {
|
|
||||||
Text(
|
|
||||||
text = errorMessage,
|
|
||||||
color = MaterialTheme.colorScheme.error,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
|
||||||
|
|
||||||
if (isFirstLogin) {
|
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
||||||
Checkbox(
|
|
||||||
checked = acceptedTerms.value,
|
|
||||||
onCheckedChange = { acceptedTerms.value = it },
|
|
||||||
)
|
|
||||||
|
|
||||||
val regularText = SpanStyle(color = MaterialTheme.colorScheme.onBackground)
|
|
||||||
|
|
||||||
val clickableTextStyle = SpanStyle(color = MaterialTheme.colorScheme.primary)
|
|
||||||
|
|
||||||
val annotatedTermsString =
|
|
||||||
buildAnnotatedString {
|
|
||||||
withStyle(regularText) { append(stringResource(R.string.i_accept_the)) }
|
|
||||||
|
|
||||||
withStyle(clickableTextStyle) {
|
|
||||||
pushStringAnnotation("openTerms", "")
|
|
||||||
append(stringResource(R.string.terms_of_use))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ClickableText(
|
|
||||||
text = annotatedTermsString,
|
|
||||||
) { spanOffset ->
|
|
||||||
annotatedTermsString.getStringAnnotations(spanOffset, spanOffset).firstOrNull()?.also {
|
|
||||||
span ->
|
|
||||||
if (span.tag == "openTerms") {
|
|
||||||
runCatching {
|
|
||||||
uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
IconButton(onClick = { dialogOpen = true }) {
|
||||||
if (termsAcceptanceIsRequired.isNotBlank()) {
|
Icon(
|
||||||
Text(
|
painter = painterResource(R.drawable.ic_qrcode),
|
||||||
text = termsAcceptanceIsRequired,
|
null,
|
||||||
color = MaterialTheme.colorScheme.error,
|
modifier = Modifier.size(24.dp),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
visualTransformation =
|
||||||
if (PackageUtils.isOrbotInstalled(context)) {
|
if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
keyboardActions =
|
||||||
Checkbox(
|
KeyboardActions(
|
||||||
checked = useProxy.value,
|
onGo = {
|
||||||
onCheckedChange = {
|
|
||||||
if (it) {
|
|
||||||
connectOrbotDialogOpen = true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(stringResource(R.string.connect_via_tor))
|
|
||||||
}
|
|
||||||
|
|
||||||
if (connectOrbotDialogOpen) {
|
|
||||||
ConnectOrbotDialog(
|
|
||||||
onClose = { connectOrbotDialogOpen = false },
|
|
||||||
onPost = {
|
|
||||||
connectOrbotDialogOpen = false
|
|
||||||
useProxy.value = true
|
|
||||||
},
|
|
||||||
onError = {
|
|
||||||
scope.launch {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
it,
|
|
||||||
Toast.LENGTH_LONG,
|
|
||||||
)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
proxyPort,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(20.dp))
|
|
||||||
|
|
||||||
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
|
|
||||||
Button(
|
|
||||||
enabled = acceptedTerms.value,
|
|
||||||
onClick = {
|
|
||||||
if (!acceptedTerms.value) {
|
if (!acceptedTerms.value) {
|
||||||
termsAcceptanceIsRequired =
|
termsAcceptanceIsRequired =
|
||||||
context.getString(R.string.acceptance_of_terms_is_required)
|
context.getString(R.string.acceptance_of_terms_is_required)
|
||||||
@ -436,60 +328,171 @@ fun LoginPage(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (errorMessage.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = errorMessage,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
if (isFirstLogin) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(
|
||||||
|
checked = acceptedTerms.value,
|
||||||
|
onCheckedChange = { acceptedTerms.value = it },
|
||||||
|
)
|
||||||
|
|
||||||
|
val regularText = SpanStyle(color = MaterialTheme.colorScheme.onBackground)
|
||||||
|
|
||||||
|
val clickableTextStyle = SpanStyle(color = MaterialTheme.colorScheme.primary)
|
||||||
|
|
||||||
|
val annotatedTermsString =
|
||||||
|
buildAnnotatedString {
|
||||||
|
withStyle(regularText) { append(stringResource(R.string.i_accept_the)) }
|
||||||
|
|
||||||
|
withStyle(clickableTextStyle) {
|
||||||
|
pushStringAnnotation("openTerms", "")
|
||||||
|
append(stringResource(R.string.terms_of_use))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableText(
|
||||||
|
text = annotatedTermsString,
|
||||||
|
) { spanOffset ->
|
||||||
|
annotatedTermsString.getStringAnnotations(spanOffset, spanOffset).firstOrNull()?.also {
|
||||||
|
span ->
|
||||||
|
if (span.tag == "openTerms") {
|
||||||
|
runCatching {
|
||||||
|
uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (termsAcceptanceIsRequired.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = termsAcceptanceIsRequired,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PackageUtils.isOrbotInstalled(context)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(
|
||||||
|
checked = useProxy.value,
|
||||||
|
onCheckedChange = {
|
||||||
|
if (it) {
|
||||||
|
connectOrbotDialogOpen = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(stringResource(R.string.connect_via_tor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectOrbotDialogOpen) {
|
||||||
|
ConnectOrbotDialog(
|
||||||
|
onClose = { connectOrbotDialogOpen = false },
|
||||||
|
onPost = {
|
||||||
|
connectOrbotDialogOpen = false
|
||||||
|
useProxy.value = true
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
scope.launch {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
it,
|
||||||
|
Toast.LENGTH_LONG,
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
proxyPort,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
|
||||||
|
Button(
|
||||||
|
enabled = acceptedTerms.value,
|
||||||
|
onClick = {
|
||||||
|
if (!acceptedTerms.value) {
|
||||||
|
termsAcceptanceIsRequired =
|
||||||
|
context.getString(R.string.acceptance_of_terms_is_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.value.text.isBlank()) {
|
||||||
|
errorMessage = context.getString(R.string.key_is_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acceptedTerms.value && key.value.text.isNotBlank()) {
|
||||||
|
accountViewModel.login(key.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||||
|
errorMessage = context.getString(R.string.invalid_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(Size35dp),
|
||||||
|
modifier = Modifier.height(50.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.login),
|
||||||
|
modifier = Modifier.padding(horizontal = 40.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PackageUtils.isAmberInstalled(context)) {
|
||||||
|
Box(modifier = Modifier.padding(40.dp, 40.dp, 40.dp, 0.dp)) {
|
||||||
|
Button(
|
||||||
|
enabled = acceptedTerms.value,
|
||||||
|
onClick = {
|
||||||
|
if (!acceptedTerms.value) {
|
||||||
|
termsAcceptanceIsRequired =
|
||||||
|
context.getString(R.string.acceptance_of_terms_is_required)
|
||||||
|
return@Button
|
||||||
|
}
|
||||||
|
|
||||||
|
loginWithExternalSigner = true
|
||||||
|
return@Button
|
||||||
|
},
|
||||||
shape = RoundedCornerShape(Size35dp),
|
shape = RoundedCornerShape(Size35dp),
|
||||||
modifier = Modifier.height(50.dp),
|
modifier = Modifier.height(50.dp),
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.login),
|
text = stringResource(R.string.login_with_external_signer),
|
||||||
modifier = Modifier.padding(horizontal = 40.dp),
|
modifier = Modifier.padding(horizontal = 40.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (PackageUtils.isAmberInstalled(context)) {
|
|
||||||
Box(modifier = Modifier.padding(40.dp, 40.dp, 40.dp, 0.dp)) {
|
|
||||||
Button(
|
|
||||||
enabled = acceptedTerms.value,
|
|
||||||
onClick = {
|
|
||||||
if (!acceptedTerms.value) {
|
|
||||||
termsAcceptanceIsRequired =
|
|
||||||
context.getString(R.string.acceptance_of_terms_is_required)
|
|
||||||
return@Button
|
|
||||||
}
|
|
||||||
|
|
||||||
loginWithExternalSigner = true
|
|
||||||
return@Button
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(Size35dp),
|
|
||||||
modifier = Modifier.height(50.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.login_with_external_signer),
|
|
||||||
modifier = Modifier.padding(horizontal = 40.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The last child is glued to the bottom.
|
Spacer(modifier = Modifier.height(Size40dp))
|
||||||
ClickableText(
|
|
||||||
text = AnnotatedString(stringResource(R.string.generate_a_new_key)),
|
Text(text = stringResource(R.string.don_t_have_an_account))
|
||||||
modifier = Modifier.padding(20.dp).fillMaxWidth(),
|
|
||||||
onClick = {
|
Spacer(modifier = Modifier.height(Size20dp))
|
||||||
if (acceptedTerms.value) {
|
|
||||||
accountViewModel.newKey(useProxy.value, proxyPort.value.toInt())
|
Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) {
|
||||||
} else {
|
Button(
|
||||||
termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)
|
onClick = onWantsToLogin,
|
||||||
}
|
shape = RoundedCornerShape(Size35dp),
|
||||||
},
|
modifier = Modifier.height(50.dp),
|
||||||
style =
|
) {
|
||||||
TextStyle(
|
Text(
|
||||||
fontSize = Font14SP,
|
text = stringResource(R.string.sign_up),
|
||||||
textDecoration = TextDecoration.Underline,
|
modifier = Modifier.padding(horizontal = Size40dp),
|
||||||
color = MaterialTheme.colorScheme.primary,
|
)
|
||||||
textAlign = TextAlign.Center,
|
}
|
||||||
),
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,299 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2023 Vitor Pamplona
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
* this software and associated documentation files (the "Software"), to deal in
|
||||||
|
* the Software without restriction, including without limitation the rights to use,
|
||||||
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
* subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in all
|
||||||
|
* copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||||
|
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
package com.vitorpamplona.amethyst.ui.screen.loggedOff
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.ClickableText
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
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.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
|
import com.vitorpamplona.amethyst.service.PackageUtils
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.ConnectOrbotDialog
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size20dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size40dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SignUpPage() {
|
||||||
|
val accountViewModel: AccountStateViewModel = viewModel()
|
||||||
|
|
||||||
|
SignUpPage(accountViewModel) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SignUpPage(
|
||||||
|
accountViewModel: AccountStateViewModel,
|
||||||
|
onWantsToLogin: () -> Unit,
|
||||||
|
) {
|
||||||
|
val displayName = remember { mutableStateOf(TextFieldValue("")) }
|
||||||
|
var errorMessage by remember { mutableStateOf("") }
|
||||||
|
val acceptedTerms = remember { mutableStateOf(false) }
|
||||||
|
var termsAcceptanceIsRequired by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
val uri = LocalUriHandler.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
val useProxy = remember { mutableStateOf(false) }
|
||||||
|
val proxyPort = remember { mutableStateOf("9050") }
|
||||||
|
var connectOrbotDialogOpen by remember { mutableStateOf(false) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(Size20dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painterResource(id = R.drawable.amethyst),
|
||||||
|
contentDescription = stringResource(R.string.app_logo),
|
||||||
|
modifier = Modifier.size(150.dp),
|
||||||
|
contentScale = ContentScale.Inside,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Size40dp))
|
||||||
|
|
||||||
|
Text(text = stringResource(R.string.welcome), style = MaterialTheme.typography.titleLarge)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Size20dp))
|
||||||
|
|
||||||
|
Text(text = stringResource(R.string.how_should_we_call_you), style = MaterialTheme.typography.titleMedium)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Size20dp))
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = displayName.value,
|
||||||
|
onValueChange = { displayName.value = it },
|
||||||
|
keyboardOptions =
|
||||||
|
KeyboardOptions(
|
||||||
|
autoCorrect = false,
|
||||||
|
keyboardType = KeyboardType.Text,
|
||||||
|
imeAction = ImeAction.Go,
|
||||||
|
),
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.my_awesome_name),
|
||||||
|
color = MaterialTheme.colorScheme.placeholderText,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
keyboardActions =
|
||||||
|
KeyboardActions(
|
||||||
|
onGo = {
|
||||||
|
if (!acceptedTerms.value) {
|
||||||
|
termsAcceptanceIsRequired =
|
||||||
|
context.getString(R.string.acceptance_of_terms_is_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayName.value.text.isBlank()) {
|
||||||
|
errorMessage = context.getString(R.string.name_is_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acceptedTerms.value && displayName.value.text.isNotBlank()) {
|
||||||
|
accountViewModel.login(displayName.value.text, useProxy.value, proxyPort.value.toInt()) {
|
||||||
|
errorMessage = context.getString(R.string.invalid_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (errorMessage.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = errorMessage,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(
|
||||||
|
checked = acceptedTerms.value,
|
||||||
|
onCheckedChange = { acceptedTerms.value = it },
|
||||||
|
)
|
||||||
|
|
||||||
|
val regularText = SpanStyle(color = MaterialTheme.colorScheme.onBackground)
|
||||||
|
|
||||||
|
val clickableTextStyle = SpanStyle(color = MaterialTheme.colorScheme.primary)
|
||||||
|
|
||||||
|
val annotatedTermsString =
|
||||||
|
buildAnnotatedString {
|
||||||
|
withStyle(regularText) { append(stringResource(R.string.i_accept_the)) }
|
||||||
|
|
||||||
|
withStyle(clickableTextStyle) {
|
||||||
|
pushStringAnnotation("openTerms", "")
|
||||||
|
append(stringResource(R.string.terms_of_use))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableText(
|
||||||
|
text = annotatedTermsString,
|
||||||
|
) { spanOffset ->
|
||||||
|
annotatedTermsString.getStringAnnotations(spanOffset, spanOffset).firstOrNull()?.also {
|
||||||
|
span ->
|
||||||
|
if (span.tag == "openTerms") {
|
||||||
|
runCatching {
|
||||||
|
uri.openUri("https://github.com/vitorpamplona/amethyst/blob/main/PRIVACY.md")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (termsAcceptanceIsRequired.isNotBlank()) {
|
||||||
|
Text(
|
||||||
|
text = termsAcceptanceIsRequired,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PackageUtils.isOrbotInstalled(context)) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(
|
||||||
|
checked = useProxy.value,
|
||||||
|
onCheckedChange = {
|
||||||
|
if (it) {
|
||||||
|
connectOrbotDialogOpen = true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(stringResource(R.string.connect_via_tor))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectOrbotDialogOpen) {
|
||||||
|
ConnectOrbotDialog(
|
||||||
|
onClose = { connectOrbotDialogOpen = false },
|
||||||
|
onPost = {
|
||||||
|
connectOrbotDialogOpen = false
|
||||||
|
useProxy.value = true
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
scope.launch {
|
||||||
|
Toast.makeText(
|
||||||
|
context,
|
||||||
|
it,
|
||||||
|
Toast.LENGTH_LONG,
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
proxyPort,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Size20dp))
|
||||||
|
|
||||||
|
Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) {
|
||||||
|
Button(
|
||||||
|
enabled = acceptedTerms.value,
|
||||||
|
onClick = {
|
||||||
|
if (!acceptedTerms.value) {
|
||||||
|
termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (displayName.value.text.isBlank()) {
|
||||||
|
errorMessage = context.getString(R.string.key_is_required)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (acceptedTerms.value && displayName.value.text.isNotBlank()) {
|
||||||
|
accountViewModel.newKey(useProxy.value, proxyPort.value.toInt(), displayName.value.text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
shape = RoundedCornerShape(Size35dp),
|
||||||
|
modifier = Modifier.height(50.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.create_account),
|
||||||
|
modifier = Modifier.padding(horizontal = Size40dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Size40dp))
|
||||||
|
|
||||||
|
Text(text = stringResource(R.string.already_have_an_account))
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(Size20dp))
|
||||||
|
|
||||||
|
Box(modifier = Modifier.padding(Size40dp, 0.dp, Size40dp, 0.dp)) {
|
||||||
|
Button(
|
||||||
|
onClick = onWantsToLogin,
|
||||||
|
shape = RoundedCornerShape(Size35dp),
|
||||||
|
modifier = Modifier.height(50.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.login),
|
||||||
|
modifier = Modifier.padding(horizontal = Size40dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -93,6 +93,8 @@
|
|||||||
<string name="add_a_relay">Add a Relay</string>
|
<string name="add_a_relay">Add a Relay</string>
|
||||||
<string name="display_name">Display Name</string>
|
<string name="display_name">Display Name</string>
|
||||||
<string name="my_display_name">My display name</string>
|
<string name="my_display_name">My display name</string>
|
||||||
|
<string name="my_awesome_name">Ostrich McAwesome</string>
|
||||||
|
<string name="welcome">Welcome Ostrich!</string>
|
||||||
<string name="username">Username</string>
|
<string name="username">Username</string>
|
||||||
<string name="my_username">My username</string>
|
<string name="my_username">My username</string>
|
||||||
<string name="about_me">About me</string>
|
<string name="about_me">About me</string>
|
||||||
@ -146,7 +148,14 @@
|
|||||||
<string name="terms_of_use">terms of use</string>
|
<string name="terms_of_use">terms of use</string>
|
||||||
<string name="acceptance_of_terms_is_required">Acceptance of terms is required</string>
|
<string name="acceptance_of_terms_is_required">Acceptance of terms is required</string>
|
||||||
<string name="key_is_required">Key is required</string>
|
<string name="key_is_required">Key is required</string>
|
||||||
|
<string name="name_is_required">A name is required</string>
|
||||||
<string name="login">Login</string>
|
<string name="login">Login</string>
|
||||||
|
<string name="sign_up">Sign Up</string>
|
||||||
|
<string name="create_account">Create Account</string>
|
||||||
|
<string name="how_should_we_call_you">How should we call you?</string>
|
||||||
|
<string name="don_t_have_an_account">Don\'t have a Nostr account?</string>
|
||||||
|
<string name="already_have_an_account">Already have a Nostr account?</string>
|
||||||
|
<string name="create_a_new_account">Create a new account</string>
|
||||||
<string name="generate_a_new_key">Generate a new key</string>
|
<string name="generate_a_new_key">Generate a new key</string>
|
||||||
<string name="loading_feed">Loading feed</string>
|
<string name="loading_feed">Loading feed</string>
|
||||||
<string name="loading_account">Loading account</string>
|
<string name="loading_account">Loading account</string>
|
||||||
@ -525,7 +534,7 @@
|
|||||||
<string name="messages_new_subject_message_placeholder">Changing the name for the new goals.</string>
|
<string name="messages_new_subject_message_placeholder">Changing the name for the new goals.</string>
|
||||||
|
|
||||||
<string name="paste_from_clipboard">Paste from clipboard</string>
|
<string name="paste_from_clipboard">Paste from clipboard</string>
|
||||||
|
|
||||||
<string name="language_description">For the App\'s Interface</string>
|
<string name="language_description">For the App\'s Interface</string>
|
||||||
<string name="theme_description">Dark, Light or System theme</string>
|
<string name="theme_description">Dark, Light or System theme</string>
|
||||||
<string name="automatically_load_images_gifs_description">Automatically load images and GIFs</string>
|
<string name="automatically_load_images_gifs_description">Automatically load images and GIFs</string>
|
||||||
|
@ -22,10 +22,13 @@ package com.vitorpamplona.quartz.events
|
|||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.node.ObjectNode
|
||||||
import com.vitorpamplona.quartz.encoders.HexKey
|
import com.vitorpamplona.quartz.encoders.HexKey
|
||||||
import com.vitorpamplona.quartz.signers.NostrSigner
|
import com.vitorpamplona.quartz.signers.NostrSigner
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.StringWriter
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
abstract class IdentityClaim(
|
abstract class IdentityClaim(
|
||||||
@ -176,23 +179,115 @@ class MetadataEvent(
|
|||||||
companion object {
|
companion object {
|
||||||
const val KIND = 0
|
const val KIND = 0
|
||||||
|
|
||||||
fun create(
|
fun updateFromPast(
|
||||||
contactMetaData: String,
|
latest: MetadataEvent?,
|
||||||
newName: String,
|
name: String?,
|
||||||
identities: List<IdentityClaim>,
|
picture: String?,
|
||||||
|
banner: String?,
|
||||||
|
website: String?,
|
||||||
|
about: String?,
|
||||||
|
nip05: String?,
|
||||||
|
lnAddress: String?,
|
||||||
|
lnURL: String?,
|
||||||
|
twitter: String?,
|
||||||
|
mastodon: String?,
|
||||||
|
github: String?,
|
||||||
signer: NostrSigner,
|
signer: NostrSigner,
|
||||||
createdAt: Long = TimeUtils.now(),
|
createdAt: Long = TimeUtils.now(),
|
||||||
onReady: (MetadataEvent) -> Unit,
|
onReady: (MetadataEvent) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
// Tries to not delete any existing attribute that we do not work with.
|
||||||
|
val currentJson =
|
||||||
|
if (latest != null) {
|
||||||
|
ObjectMapper()
|
||||||
|
.readTree(
|
||||||
|
ByteArrayInputStream(latest.content.toByteArray(Charsets.UTF_8)),
|
||||||
|
) as ObjectNode
|
||||||
|
} else {
|
||||||
|
ObjectMapper().createObjectNode()
|
||||||
|
}
|
||||||
|
|
||||||
|
name?.let { addIfNotBlank(currentJson, "name", it.trim()) }
|
||||||
|
name?.let { addIfNotBlank(currentJson, "display_name", it.trim()) }
|
||||||
|
picture?.let { addIfNotBlank(currentJson, "picture", it.trim()) }
|
||||||
|
banner?.let { addIfNotBlank(currentJson, "banner", it.trim()) }
|
||||||
|
website?.let { addIfNotBlank(currentJson, "website", it.trim()) }
|
||||||
|
about?.let { addIfNotBlank(currentJson, "about", it.trim()) }
|
||||||
|
nip05?.let { addIfNotBlank(currentJson, "nip05", it.trim()) }
|
||||||
|
lnAddress?.let { addIfNotBlank(currentJson, "lud16", it.trim()) }
|
||||||
|
lnURL?.let { addIfNotBlank(currentJson, "lud06", it.trim()) }
|
||||||
|
|
||||||
|
var claims = latest?.identityClaims() ?: emptyList()
|
||||||
|
|
||||||
|
if (twitter?.isBlank() == true) {
|
||||||
|
// delete twitter
|
||||||
|
claims = claims.filter { it !is TwitterIdentity }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (github?.isBlank() == true) {
|
||||||
|
// delete github
|
||||||
|
claims = claims.filter { it !is GitHubIdentity }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mastodon?.isBlank() == true) {
|
||||||
|
// delete mastodon
|
||||||
|
claims = claims.filter { it !is MastodonIdentity }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updates while keeping other identities intact
|
||||||
|
val newClaims =
|
||||||
|
listOfNotNull(
|
||||||
|
twitter?.let { TwitterIdentity.parseProofUrl(it) },
|
||||||
|
github?.let { GitHubIdentity.parseProofUrl(it) },
|
||||||
|
mastodon?.let { MastodonIdentity.parseProofUrl(it) },
|
||||||
|
) +
|
||||||
|
claims.filter { it !is TwitterIdentity && it !is GitHubIdentity && it !is MastodonIdentity }
|
||||||
|
|
||||||
|
val writer = StringWriter()
|
||||||
|
ObjectMapper().writeValue(writer, currentJson)
|
||||||
|
|
||||||
|
val tags = mutableListOf<Array<String>>()
|
||||||
|
|
||||||
|
tags.add(
|
||||||
|
arrayOf("alt", "User profile for ${name ?: currentJson.get("name").asText() ?: ""}"),
|
||||||
|
)
|
||||||
|
|
||||||
|
newClaims.forEach { tags.add(arrayOf("i", it.platformIdentity(), it.proof)) }
|
||||||
|
|
||||||
|
signer.sign(createdAt, KIND, tags.toTypedArray(), writer.buffer.toString(), onReady)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addIfNotBlank(
|
||||||
|
currentJson: ObjectNode,
|
||||||
|
key: String,
|
||||||
|
value: String,
|
||||||
|
) {
|
||||||
|
if (value.isBlank()) {
|
||||||
|
currentJson.remove(key)
|
||||||
|
} else {
|
||||||
|
currentJson.put(key, value.trim())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createFromScratch(
|
||||||
|
newName: String,
|
||||||
|
signer: NostrSigner,
|
||||||
|
createdAt: Long = TimeUtils.now(),
|
||||||
|
onReady: (MetadataEvent) -> Unit,
|
||||||
|
) {
|
||||||
|
val prop = ObjectMapper().createObjectNode()
|
||||||
|
prop.put("name", newName.trim())
|
||||||
|
|
||||||
|
val writer = StringWriter()
|
||||||
|
ObjectMapper().writeValue(writer, prop)
|
||||||
|
|
||||||
val tags = mutableListOf<Array<String>>()
|
val tags = mutableListOf<Array<String>>()
|
||||||
|
|
||||||
tags.add(
|
tags.add(
|
||||||
arrayOf("alt", "User profile for $newName"),
|
arrayOf("alt", "User profile for $newName"),
|
||||||
)
|
)
|
||||||
|
|
||||||
identities.forEach { tags.add(arrayOf("i", it.platformIdentity(), it.proof)) }
|
signer.sign(createdAt, KIND, tags.toTypedArray(), writer.buffer.toString(), onReady)
|
||||||
|
|
||||||
signer.sign(createdAt, KIND, tags.toTypedArray(), contactMetaData, onReady)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user