mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2025-03-17 21:31:57 +01: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/appInsightsSettings.xml
|
||||
/.idea/ktlint-plugin.xml
|
||||
/.idea/ktfmt.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
|
@ -130,8 +130,11 @@ object LocalPreferences {
|
||||
return currentAccount
|
||||
}
|
||||
|
||||
private suspend fun updateCurrentAccount(npub: String) {
|
||||
if (currentAccount != npub) {
|
||||
private fun updateCurrentAccount(npub: String?) {
|
||||
if (npub == null) {
|
||||
currentAccount = null
|
||||
encryptedPreferences().edit().clear().apply()
|
||||
} else if (currentAccount != npub) {
|
||||
currentAccount = npub
|
||||
|
||||
encryptedPreferences().edit().apply { putString(PrefKeys.CURRENT_ACCOUNT, npub) }.apply()
|
||||
@ -250,7 +253,7 @@ object LocalPreferences {
|
||||
deleteUserPreferenceFile(accountInfo.npub)
|
||||
|
||||
if (savedAccounts().isEmpty()) {
|
||||
encryptedPreferences().edit().clear().apply()
|
||||
updateCurrentAccount(null)
|
||||
} else if (currentAccount() == accountInfo.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.GiftWrapEvent
|
||||
import com.vitorpamplona.quartz.events.HTTPAuthorizationEvent
|
||||
import com.vitorpamplona.quartz.events.IdentityClaim
|
||||
import com.vitorpamplona.quartz.events.LiveActivitiesChatMessageEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapEvent
|
||||
import com.vitorpamplona.quartz.events.LnZapPaymentRequestEvent
|
||||
@ -541,14 +540,36 @@ class Account(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun sendNewUserMetadata(
|
||||
toString: String,
|
||||
newName: String,
|
||||
identities: List<IdentityClaim>,
|
||||
fun sendNewUserMetadata(
|
||||
name: String? = null,
|
||||
picture: String? = null,
|
||||
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
|
||||
|
||||
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)
|
||||
LocalCache.justConsume(it, null)
|
||||
}
|
||||
|
@ -27,8 +27,6 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.ViewModel
|
||||
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.service.Nip96Uploader
|
||||
import com.vitorpamplona.amethyst.ui.components.MediaCompressor
|
||||
@ -39,8 +37,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.StringWriter
|
||||
|
||||
class NewUserMetadataViewModel : ViewModel() {
|
||||
private lateinit var account: Account
|
||||
@ -97,59 +93,22 @@ class NewUserMetadataViewModel : ViewModel() {
|
||||
|
||||
fun create() {
|
||||
// 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) {
|
||||
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() {
|
||||
|
@ -73,7 +73,7 @@ import com.vitorpamplona.amethyst.ui.note.ArrowBackIcon
|
||||
import com.vitorpamplona.amethyst.ui.note.toShortenHex
|
||||
import com.vitorpamplona.amethyst.ui.screen.AccountStateViewModel
|
||||
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.Size10dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.Size55dp
|
||||
@ -121,7 +121,7 @@ fun AccountSwitchBottomSheet(
|
||||
) {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
Box {
|
||||
LoginPage(accountStateViewModel, isFirstLogin = false)
|
||||
LoginOrSignupScreen(accountStateViewModel, isFirstLogin = false)
|
||||
TopAppBar(
|
||||
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.screen.loggedIn.AccountViewModel
|
||||
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 kotlinx.coroutines.launch
|
||||
|
||||
@ -71,7 +71,7 @@ fun AccountScreen(
|
||||
LoadingAccounts()
|
||||
}
|
||||
is AccountState.LoggedOff -> {
|
||||
LoginPage(accountStateViewModel, isFirstLogin = true)
|
||||
LoginOrSignupScreen(accountStateViewModel, isFirstLogin = true)
|
||||
}
|
||||
is AccountState.LoggedIn -> {
|
||||
CompositionLocalProvider(
|
||||
|
@ -29,6 +29,7 @@ import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.ServiceManager
|
||||
import com.vitorpamplona.amethyst.model.Account
|
||||
import com.vitorpamplona.amethyst.service.HttpClient
|
||||
import com.vitorpamplona.amethyst.service.relays.Client
|
||||
import com.vitorpamplona.quartz.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.encoders.Hex
|
||||
import com.vitorpamplona.quartz.encoders.Nip19
|
||||
@ -42,6 +43,7 @@ import com.vitorpamplona.quartz.signers.NostrSignerInternal
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
@ -65,8 +67,7 @@ class AccountStateViewModel() : ViewModel() {
|
||||
|
||||
private suspend fun tryLoginExistingAccount() =
|
||||
withContext(Dispatchers.IO) {
|
||||
LocalPreferences.loadCurrentAccountFromEncryptedStorage()?.let { startUI(it) }
|
||||
?: run { requestLoginUI() }
|
||||
LocalPreferences.loadCurrentAccountFromEncryptedStorage()?.let { startUI(it) } ?: run { requestLoginUI() }
|
||||
}
|
||||
|
||||
private suspend fun requestLoginUI() {
|
||||
@ -144,25 +145,33 @@ class AccountStateViewModel() : ViewModel() {
|
||||
startUI(account)
|
||||
}
|
||||
|
||||
suspend fun startUI(account: Account) =
|
||||
withContext(Dispatchers.Main) {
|
||||
if (account.isWriteable()) {
|
||||
_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)
|
||||
suspend fun startUI(
|
||||
account: Account,
|
||||
onServicesReady: (() -> Unit)? = null,
|
||||
) = withContext(Dispatchers.Main) {
|
||||
if (account.isWriteable()) {
|
||||
_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)
|
||||
|
||||
if (onServicesReady != null) {
|
||||
// waits for the connection to go through
|
||||
delay(1000)
|
||||
onServicesReady()
|
||||
}
|
||||
}
|
||||
|
||||
account.saveable.observeForever(saveListener)
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private val saveListener: (com.vitorpamplona.amethyst.model.AccountState) -> Unit = {
|
||||
GlobalScope.launch(Dispatchers.IO) { LocalPreferences.saveToEncryptedStorage(it.account) }
|
||||
@ -204,6 +213,7 @@ class AccountStateViewModel() : ViewModel() {
|
||||
fun newKey(
|
||||
useProxy: Boolean,
|
||||
proxyPort: Int,
|
||||
name: String? = null,
|
||||
) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val proxy = HttpClient.initProxy(useProxy, "127.0.0.1", proxyPort)
|
||||
@ -220,7 +230,10 @@ class AccountStateViewModel() : ViewModel() {
|
||||
|
||||
// saves to local preferences
|
||||
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.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
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.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
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.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.R
|
||||
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.screen.AccountStateViewModel
|
||||
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.Size40dp
|
||||
import com.vitorpamplona.amethyst.ui.theme.placeholderText
|
||||
import com.vitorpamplona.quartz.signers.ExternalSignerLauncher
|
||||
import com.vitorpamplona.quartz.signers.SignerType
|
||||
@ -105,11 +103,21 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.UUID
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun LoginPage() {
|
||||
val accountViewModel: AccountStateViewModel = viewModel()
|
||||
|
||||
LoginPage(accountViewModel, true) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun LoginPage(
|
||||
accountViewModel: AccountStateViewModel,
|
||||
isFirstLogin: Boolean,
|
||||
onWantsToLogin: () -> Unit,
|
||||
) {
|
||||
val key = remember { mutableStateOf(TextFieldValue("")) }
|
||||
var errorMessage by remember { mutableStateOf("") }
|
||||
@ -208,219 +216,103 @@ fun LoginPage(
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(Size20dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
// The first child is glued to the top.
|
||||
// Hence we have nothing at the top, an empty box is used.
|
||||
Box(modifier = Modifier.height(0.dp))
|
||||
Image(
|
||||
painterResource(id = R.drawable.amethyst),
|
||||
contentDescription = stringResource(R.string.app_logo),
|
||||
modifier = Modifier.size(150.dp),
|
||||
contentScale = ContentScale.Inside,
|
||||
)
|
||||
|
||||
// The second child, this column, is centered vertically.
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp).fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
painterResource(id = R.drawable.amethyst),
|
||||
contentDescription = stringResource(R.string.app_logo),
|
||||
modifier = Modifier.size(200.dp),
|
||||
contentScale = ContentScale.Inside,
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
var showPassword by remember { mutableStateOf(false) }
|
||||
|
||||
val autofillNode =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { key.value = TextFieldValue(it) },
|
||||
)
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNode
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
var showPassword by remember { mutableStateOf(false) }
|
||||
|
||||
val autofillNode =
|
||||
AutofillNode(
|
||||
autofillTypes = listOf(AutofillType.Password),
|
||||
onFill = { key.value = TextFieldValue(it) },
|
||||
)
|
||||
val autofill = LocalAutofill.current
|
||||
LocalAutofillTree.current += autofillNode
|
||||
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier.onGloballyPositioned { coordinates ->
|
||||
OutlinedTextField(
|
||||
modifier =
|
||||
Modifier
|
||||
.onGloballyPositioned { coordinates ->
|
||||
autofillNode.boundingBox = coordinates.boundsInWindow()
|
||||
}
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNode)
|
||||
} else {
|
||||
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)
|
||||
.onFocusChanged { focusState ->
|
||||
autofill?.run {
|
||||
if (focusState.isFocused) {
|
||||
requestAutofillForNode(autofillNode)
|
||||
} else {
|
||||
cancelAutofillForNode(autofillNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
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(
|
||||
painter = painterResource(R.drawable.ic_qrcode),
|
||||
null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
imageVector =
|
||||
if (showPassword) Icons.Outlined.VisibilityOff else Icons.Outlined.Visibility,
|
||||
contentDescription =
|
||||
if (showPassword) {
|
||||
stringResource(R.string.show_password)
|
||||
} else {
|
||||
stringResource(
|
||||
R.string.hide_password,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
},
|
||||
visualTransformation =
|
||||
if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onGo = {
|
||||
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)
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
leadingIcon = {
|
||||
if (dialogOpen) {
|
||||
SimpleQrCodeScanner {
|
||||
dialogOpen = false
|
||||
if (!it.isNullOrEmpty()) {
|
||||
key.value = TextFieldValue(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (termsAcceptanceIsRequired.isNotBlank()) {
|
||||
Text(
|
||||
text = termsAcceptanceIsRequired,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
IconButton(onClick = { dialogOpen = true }) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_qrcode),
|
||||
null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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 = {
|
||||
},
|
||||
visualTransformation =
|
||||
if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
|
||||
keyboardActions =
|
||||
KeyboardActions(
|
||||
onGo = {
|
||||
if (!acceptedTerms.value) {
|
||||
termsAcceptanceIsRequired =
|
||||
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),
|
||||
modifier = Modifier.height(50.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.login),
|
||||
text = stringResource(R.string.login_with_external_signer),
|
||||
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.
|
||||
ClickableText(
|
||||
text = AnnotatedString(stringResource(R.string.generate_a_new_key)),
|
||||
modifier = Modifier.padding(20.dp).fillMaxWidth(),
|
||||
onClick = {
|
||||
if (acceptedTerms.value) {
|
||||
accountViewModel.newKey(useProxy.value, proxyPort.value.toInt())
|
||||
} else {
|
||||
termsAcceptanceIsRequired = context.getString(R.string.acceptance_of_terms_is_required)
|
||||
}
|
||||
},
|
||||
style =
|
||||
TextStyle(
|
||||
fontSize = Font14SP,
|
||||
textDecoration = TextDecoration.Underline,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(Size40dp))
|
||||
|
||||
Text(text = stringResource(R.string.don_t_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.sign_up),
|
||||
modifier = Modifier.padding(horizontal = Size40dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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="display_name">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="my_username">My username</string>
|
||||
<string name="about_me">About me</string>
|
||||
@ -146,7 +148,14 @@
|
||||
<string name="terms_of_use">terms of use</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="name_is_required">A name is required</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="loading_feed">Loading feed</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="paste_from_clipboard">Paste from clipboard</string>
|
||||
|
||||
|
||||
<string name="language_description">For the App\'s Interface</string>
|
||||
<string name="theme_description">Dark, Light or System theme</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 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.signers.NostrSigner
|
||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.StringWriter
|
||||
|
||||
@Stable
|
||||
abstract class IdentityClaim(
|
||||
@ -176,23 +179,115 @@ class MetadataEvent(
|
||||
companion object {
|
||||
const val KIND = 0
|
||||
|
||||
fun create(
|
||||
contactMetaData: String,
|
||||
newName: String,
|
||||
identities: List<IdentityClaim>,
|
||||
fun updateFromPast(
|
||||
latest: MetadataEvent?,
|
||||
name: String?,
|
||||
picture: String?,
|
||||
banner: String?,
|
||||
website: String?,
|
||||
about: String?,
|
||||
nip05: String?,
|
||||
lnAddress: String?,
|
||||
lnURL: String?,
|
||||
twitter: String?,
|
||||
mastodon: String?,
|
||||
github: String?,
|
||||
signer: NostrSigner,
|
||||
createdAt: Long = TimeUtils.now(),
|
||||
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>>()
|
||||
|
||||
tags.add(
|
||||
arrayOf("alt", "User profile for $newName"),
|
||||
)
|
||||
|
||||
identities.forEach { tags.add(arrayOf("i", it.platformIdentity(), it.proof)) }
|
||||
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), contactMetaData, onReady)
|
||||
signer.sign(createdAt, KIND, tags.toTypedArray(), writer.buffer.toString(), onReady)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user