diff --git a/app/src/main/java/to/bitkit/ui/components/AddLinkSheet.kt b/app/src/main/java/to/bitkit/ui/components/AddLinkSheet.kt index b73a9bffcd..a88dabaa12 100644 --- a/app/src/main/java/to/bitkit/ui/components/AddLinkSheet.kt +++ b/app/src/main/java/to/bitkit/ui/components/AddLinkSheet.kt @@ -165,17 +165,27 @@ internal fun SuggestionsContent( .sheetHeight(SheetSize.COMPACT, isModal = true) .gradientBackground() .navigationBarsPadding() - .padding(horizontal = 16.dp), ) { - SheetTopBar(titleText = title, onBack = onBack) + SheetTopBar( + titleText = title, + onBack = onBack, + modifier = Modifier.padding(horizontal = 4.dp) + ) VerticalSpacer(16.dp) FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) ) { suggestions.forEach { suggestion -> - TagButton(text = suggestion, onClick = { onSelect(suggestion) }) + PrimaryButton( + text = suggestion, + onClick = { onSelect(suggestion) }, + size = ButtonSize.Small, + fullWidth = false, + ) } } VerticalSpacer(24.dp) diff --git a/app/src/main/java/to/bitkit/ui/components/CenteredProfileHeader.kt b/app/src/main/java/to/bitkit/ui/components/CenteredProfileHeader.kt index 4b86c8b1df..6094181de5 100644 --- a/app/src/main/java/to/bitkit/ui/components/CenteredProfileHeader.kt +++ b/app/src/main/java/to/bitkit/ui/components/CenteredProfileHeader.kt @@ -73,7 +73,7 @@ fun CenteredProfileHeader( ) if (bio.isNotEmpty()) { - VerticalSpacer(8.dp) + VerticalSpacer(16.dp) BodyM( text = bio, color = Colors.White64, diff --git a/app/src/main/java/to/bitkit/ui/components/LinkRow.kt b/app/src/main/java/to/bitkit/ui/components/LinkRow.kt index fa2b23f8a5..625a79116f 100644 --- a/app/src/main/java/to/bitkit/ui/components/LinkRow.kt +++ b/app/src/main/java/to/bitkit/ui/components/LinkRow.kt @@ -1,13 +1,24 @@ package to.bitkit.ui.components +import android.content.Intent +import android.net.Uri +import android.util.Patterns import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.isValidEmail +import to.bitkit.utils.Logger + +private const val TAG = "LinkRow" @Composable fun LinkRow( @@ -16,6 +27,9 @@ fun LinkRow( linkIndex: Int, modifier: Modifier = Modifier, ) { + val context = LocalContext.current + val uri = remember(label, value) { value.toLinkUri(label) } + Column(modifier = modifier.fillMaxWidth()) { VerticalSpacer(16.dp) Text13Up( @@ -26,9 +40,50 @@ fun LinkRow( VerticalSpacer(8.dp) BodySSB( text = value, - modifier = Modifier.testTag("ProfileLinkValue_$linkIndex"), + modifier = Modifier + .clickableAlpha( + onClick = uri?.let { linkUri -> + { + runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, linkUri)) } + .onFailure { Logger.warn("Failed to open link '$linkUri'", it, context = TAG) } + } + } + ) + .testTag("ProfileLinkValue_$linkIndex"), ) VerticalSpacer(16.dp) HorizontalDivider() } } + +private val PHONE_REGEX = Regex("""^\+?[0-9 ().-]{7,}$""") +private const val MIN_PHONE_DIGITS = 7 +private val PHONE_LABELS = setOf("phone", "tel", "telephone", "mobile", "cell") + +private fun String.toLinkUri(label: String): Uri? { + val trimmed = trim() + val scheme = trimmed.toUri().scheme?.lowercase() + return when { + trimmed.isEmpty() -> null + trimmed.isValidEmail() -> "mailto:$trimmed".toUri() + (trimmed.startsWith('+') || label.trim().lowercase() in PHONE_LABELS) && trimmed.isPhoneNumber() -> + trimmed.toTelUri() + scheme == "tel" -> trimmed.substringAfter(':').trim().takeIf { it.isPhoneNumber() }?.toTelUri() + trimmed.contains(' ') -> null + scheme == "mailto" -> trimmed.toUri() + Patterns.WEB_URL.matcher(trimmed).matches() -> trimmed.toWebUri() + else -> null + } +} + +private fun String.isPhoneNumber() = matches(PHONE_REGEX) && count(Char::isDigit) >= MIN_PHONE_DIGITS + +private fun String.toTelUri() = "tel:${filter { it.isDigit() || it == '+' }}".toUri() + +private fun String.toWebUri(): Uri? { + val withScheme = if (contains("://")) this else "https://$this" + return withScheme.toUri().takeIf { + val isHttp = it.scheme.equals("http", ignoreCase = true) || it.scheme.equals("https", ignoreCase = true) + isHttp && it.host?.substringAfterLast('.').orEmpty().any(Char::isLetter) + } +} diff --git a/app/src/main/java/to/bitkit/ui/components/ProfileEditForm.kt b/app/src/main/java/to/bitkit/ui/components/ProfileEditForm.kt index e84f9268d3..1be5d64513 100644 --- a/app/src/main/java/to/bitkit/ui/components/ProfileEditForm.kt +++ b/app/src/main/java/to/bitkit/ui/components/ProfileEditForm.kt @@ -1,8 +1,8 @@ package to.bitkit.ui.components import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow @@ -13,35 +13,48 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.rememberHazeState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import to.bitkit.R -import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppTextFieldDefaults import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.theme.TopBarGradient private const val BIO_MAX_LENGTH = 160 @@ -63,6 +76,7 @@ fun ProfileEditForm( onSave: () -> Unit, onCancel: () -> Unit, isSaveEnabled: Boolean, + topBar: @Composable (Modifier) -> Unit, modifier: Modifier = Modifier, avatarContent: @Composable () -> Unit = {}, publicKeyLabel: String? = null, @@ -78,106 +92,75 @@ fun ProfileEditForm( val focusManager = LocalFocusManager.current val keyboardController = LocalSoftwareKeyboardController.current - Column( - modifier = modifier - .fillMaxSize() - .imePadding() - ) { + val hazeState = rememberHazeState() + val density = LocalDensity.current + var footerHeight by remember { mutableStateOf(0.dp) } + + Box(modifier = modifier.fillMaxSize()) { Column( - horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - .weight(1f) + .fillMaxSize() + .imePadding() + .hazeSource(hazeState) .verticalScroll(rememberScrollState()) .padding(horizontal = 16.dp) ) { - VerticalSpacer(16.dp) - avatarContent() - VerticalSpacer(12.dp) - - TextInput( - value = name, - onValueChange = onNameChange, - placeholder = stringResource(R.string.profile__edit_name_placeholder), - singleLine = true, - textStyle = AppTextStyles.Display.copy(textAlign = TextAlign.Center), - colors = AppTextFieldDefaults.transparent, - modifier = Modifier - .fillMaxWidth() - .testTag("ProfileEditName") + TopBarSpacer() + ProfileEditHeader( + name = name, + onNameChange = onNameChange, + publicKey = publicKey, + publicKeyLabel = resolvedPublicKeyLabel, + avatarContent = avatarContent, + nameTestTag = "ProfileEditName", ) - HorizontalDivider() - VerticalSpacer(12.dp) - - Text13Up( - text = resolvedPublicKeyLabel, - color = Colors.White64, - ) - VerticalSpacer(4.dp) - BodyMSB( - text = publicKey, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - HorizontalDivider(modifier = Modifier.padding(top = 12.dp)) - VerticalSpacer(16.dp) - Text13Up( - text = stringResource(R.string.profile__edit_bio), - color = Colors.White64, + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxWidth() - ) - VerticalSpacer(8.dp) - TextInput( - value = bio, - onValueChange = { onBioChange(it.take(BIO_MAX_LENGTH)) }, - placeholder = resolvedBioPlaceholder, - minLines = 2, - maxLines = 4, - modifier = Modifier - .fillMaxWidth() - .testTag("ProfileEditBio") - ) + ) { + HorizontalDivider() + FieldLabel(text = stringResource(R.string.profile__edit_bio)) { + TextInput( + value = bio, + onValueChange = { onBioChange(it.take(BIO_MAX_LENGTH)) }, + placeholder = resolvedBioPlaceholder, + minLines = 3, + maxLines = 4, + modifier = Modifier + .fillMaxWidth() + .testTag("ProfileEditBio") + ) + } + HorizontalDivider() - VerticalSpacer(16.dp) - links.forEachIndexed { index, link -> - HorizontalDivider(color = Colors.White10) - VerticalSpacer(8.dp) - Text13Up( - text = link.label, - color = Colors.White64, - modifier = Modifier.fillMaxWidth() - ) - VerticalSpacer(8.dp) - TextInput( - value = link.url, - onValueChange = { onLinkUrlChange(index, it) }, - placeholder = stringResource(R.string.profile__add_link_url_placeholder), - singleLine = true, - trailingIcon = { - IconButton( - onClick = { onRemoveLink(index) }, - modifier = Modifier.testTag("ProfileEditLinkRemove_$index") - ) { - Icon( - painter = painterResource(R.drawable.ic_trash), - contentDescription = null, - tint = Colors.White64, - modifier = Modifier.size(16.dp) - ) - } - }, - modifier = Modifier - .fillMaxWidth() - .border( - width = 1.dp, - color = Colors.White10, - shape = AppShapes.small, + links.forEachIndexed { index, link -> + FieldLabel(text = link.label) { + TextInput( + value = link.url, + onValueChange = { onLinkUrlChange(index, it) }, + placeholder = stringResource(R.string.profile__add_link_url_placeholder), + singleLine = true, + trailingIcon = { + IconButton( + onClick = { onRemoveLink(index) }, + modifier = Modifier.testTag("ProfileEditLinkRemove_$index") + ) { + Icon( + painter = painterResource(R.drawable.ic_trash), + contentDescription = null, + tint = Colors.White64, + modifier = Modifier.size(16.dp) + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .testTag("ProfileEditLink_$index") ) - .testTag("ProfileEditLink_$index") - ) - VerticalSpacer(8.dp) - } - Row(modifier = Modifier.fillMaxWidth()) { + } + } PrimaryButton( text = stringResource(R.string.profile__add_link), onClick = { @@ -196,79 +179,61 @@ fun ProfileEditForm( }, modifier = Modifier.testTag("ProfileEditAddLink") ) - } + HorizontalDivider() - VerticalSpacer(16.dp) - Text13Up( - text = stringResource(R.string.profile__edit_tags), - color = Colors.White64, - modifier = Modifier.fillMaxWidth() - ) - VerticalSpacer(8.dp) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - tags.forEachIndexed { index, tag -> - TagButton( - text = tag, - onClick = { onRemoveTag(index) }, - displayIconClose = true, + FieldLabel(text = stringResource(R.string.profile__edit_tags)) { + if (tags.isNotEmpty()) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + tags.forEachIndexed { index, tag -> + TagButton( + text = tag, + onClick = { onRemoveTag(index) }, + displayIconClose = true, + ) + } + } + } + PrimaryButton( + text = stringResource(R.string.profile__add_tag), + onClick = { + focusManager.clearFocus(force = true) + keyboardController?.hide() + onAddTag() + }, + size = ButtonSize.Small, + fullWidth = false, + icon = { + Icon( + painter = painterResource(R.drawable.ic_tag), + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + }, + modifier = Modifier.testTag("ProfileEditAddTag") ) } - } - VerticalSpacer(8.dp) - Row(modifier = Modifier.fillMaxWidth()) { - PrimaryButton( - text = stringResource(R.string.profile__add_tag), - onClick = { - focusManager.clearFocus(force = true) - keyboardController?.hide() - onAddTag() - }, - size = ButtonSize.Small, - fullWidth = false, - icon = { - Icon( - painter = painterResource(R.drawable.ic_tag), - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - }, - modifier = Modifier.testTag("ProfileEditAddTag") - ) - } - - VerticalSpacer(16.dp) - if (showFooterNote) { - HorizontalDivider(color = Colors.White10) - VerticalSpacer(16.dp) - BodyS( - text = resolvedFooterNote, - color = Colors.White64, - ) - } - if (onDelete != null) { - Column { - VerticalSpacer(16.dp) + if (showFooterNote) { HorizontalDivider() - VerticalSpacer(16.dp) - Text13Up( - text = stringResource(R.string.profile__edit_delete_section), + BodyS( + text = resolvedFooterNote, color = Colors.White64, modifier = Modifier.fillMaxWidth() ) - VerticalSpacer(8.dp) - Row(modifier = Modifier.fillMaxWidth()) { + } + + if (onDelete != null) { + HorizontalDivider() + FieldLabel(text = stringResource(R.string.profile__edit_delete_section)) { PrimaryButton( text = deleteLabel, onClick = onDelete, size = ButtonSize.Small, fullWidth = false, - color = Colors.White10, - enableGradient = false, contentColor = Colors.Brand, icon = { Icon( @@ -284,23 +249,28 @@ fun ProfileEditForm( } } - VerticalSpacer(32.dp) + VerticalSpacer(footerHeight + 16.dp) } + topBar( + Modifier + .align(Alignment.TopCenter) + .background(TopBarGradient) + ) + Row( horizontalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier + .align(Alignment.BottomCenter) .fillMaxWidth() - .background( - Brush.verticalGradient( - colors = listOf(Color.Transparent, Color.Black), - ) - ) + .onSizeChanged { footerHeight = with(density) { it.height.toDp() } } + .background(FooterGradient) .padding(start = 16.dp, top = 32.dp, end = 16.dp, bottom = 16.dp) ) { SecondaryButton( text = stringResource(R.string.common__cancel), onClick = onCancel, + hazeState = hazeState, modifier = Modifier .weight(1f) .testTag("ProfileEditCancel") @@ -317,6 +287,86 @@ fun ProfileEditForm( } } +@Composable +fun ProfileEditHeader( + name: String, + onNameChange: (String) -> Unit, + publicKey: String, + nameTestTag: String, + modifier: Modifier = Modifier, + publicKeyLabel: String = stringResource(R.string.profile__your_pubky), + autoFocusName: Boolean = false, + avatarContent: @Composable () -> Unit = {}, +) { + val focusRequester = remember { FocusRequester() } + + LaunchedEffect(autoFocusName) { + if (autoFocusName) focusRequester.requestFocus() + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxWidth() + .padding(16.dp) + ) { + VerticalSpacer(16.dp) + avatarContent() + VerticalSpacer(16.dp) + TextInput( + value = name, + onValueChange = onNameChange, + placeholder = stringResource(R.string.profile__edit_name_placeholder), + singleLine = true, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Words, + autoCorrectEnabled = false, + ), + visualTransformation = UppercaseTransformation, + textStyle = AppTextStyles.Display.copy(textAlign = TextAlign.Center), + colors = AppTextFieldDefaults.transparent, + placeholderColor = Colors.White32, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + .testTag(nameTestTag) + ) + VerticalSpacer(16.dp) + HorizontalDivider() + VerticalSpacer(32.dp) + Text13Up( + text = publicKeyLabel, + color = Colors.White64, + ) + VerticalSpacer(8.dp) + BodyMSB( + text = publicKey, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@Composable +private fun FieldLabel( + text: String, + content: @Composable () -> Unit, +) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + Text13Up(text = text, color = Colors.White64) + content() + } +} + +private val UppercaseTransformation = VisualTransformation { + TransformedText(AnnotatedString(it.text.map(Char::uppercaseChar).joinToString("")), OffsetMapping.Identity) +} + +private val FooterGradient = Brush.verticalGradient(listOf(Color.Transparent, Color.Black)) + data class ProfileEditLink(val label: String, val url: String) @Preview(showSystemUi = true) @@ -341,6 +391,7 @@ private fun Preview() { onSave = {}, onCancel = {}, isSaveEnabled = true, + topBar = {}, onDelete = {}, deleteLabel = "Delete Profile", ) diff --git a/app/src/main/java/to/bitkit/ui/components/TextInput.kt b/app/src/main/java/to/bitkit/ui/components/TextInput.kt index 494dd5581b..8aedae27ac 100644 --- a/app/src/main/java/to/bitkit/ui/components/TextInput.kt +++ b/app/src/main/java/to/bitkit/ui/components/TextInput.kt @@ -16,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue @@ -48,6 +49,7 @@ fun TextInput( visualTransformation: VisualTransformation = VisualTransformation.None, textStyle: TextStyle = AppTextStyles.BodySSB, colors: TextFieldColors = AppTextFieldDefaults.semiTransparent, + placeholderColor: Color = Colors.White64, inputTransform: (String) -> String = { it }, ) { var textFieldValue by remember { @@ -72,7 +74,12 @@ fun TextInput( TextField( placeholder = { placeholder?.let { - Text(placeholder, color = Colors.White64, style = textStyle) + Text( + text = placeholder, + color = placeholderColor, + style = textStyle, + modifier = Modifier.fillMaxWidth() + ) } }, isError = isError, diff --git a/app/src/main/java/to/bitkit/ui/screens/contacts/ContactDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/contacts/ContactDetailScreen.kt index 0f217ac5d9..71f33aaf09 100644 --- a/app/src/main/java/to/bitkit/ui/screens/contacts/ContactDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/contacts/ContactDetailScreen.kt @@ -42,6 +42,7 @@ import to.bitkit.ui.components.ActionButton import to.bitkit.ui.components.AddTagSheet import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BottomSheet +import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.CenteredProfileHeader import to.bitkit.ui.components.Display import to.bitkit.ui.components.FillHeight @@ -327,7 +328,7 @@ private fun ContactBody( ) ActionButton( onClick = onClickCopy, - iconRes = R.drawable.ic_copy, + iconRes = R.drawable.ic_copy_simple, modifier = Modifier.testTag("ContactCopy") ) ActionButton( @@ -344,7 +345,7 @@ private fun ContactBody( } else { ActionButton( onClick = onClickEdit, - iconRes = R.drawable.ic_edit, + iconRes = R.drawable.ic_pencil, modifier = Modifier.testTag("ContactEdit") ) } @@ -367,27 +368,36 @@ private fun ContactBody( .testTag("ContactViewTagsHeader") ) VerticalSpacer(8.dp) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - tags.forEach { tag -> - TagButton( - text = tag, - onClick = { onRemoveTag(tag) }, - accessibilityLabel = stringResource(R.string.common__remove_tag, tag), - displayIconClose = true, - ) + if (tags.isNotEmpty()) { + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + tags.forEach { tag -> + TagButton( + text = tag, + onClick = { onRemoveTag(tag) }, + accessibilityLabel = stringResource(R.string.common__remove_tag, tag), + displayIconClose = true, + ) + } } + VerticalSpacer(8.dp) } - VerticalSpacer(8.dp) Row(modifier = Modifier.fillMaxWidth()) { - TagButton( + PrimaryButton( text = stringResource(R.string.profile__add_tag), onClick = onAddTag, - icon = painterResource(R.drawable.ic_tag), - displayIconClose = true, + size = ButtonSize.Small, + fullWidth = false, + icon = { + Icon( + painter = painterResource(R.drawable.ic_tag), + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + }, modifier = Modifier.testTag("ContactAddTag") ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/contacts/EditContactScreen.kt b/app/src/main/java/to/bitkit/ui/screens/contacts/EditContactScreen.kt index 70820c97d2..f4ec34d266 100644 --- a/app/src/main/java/to/bitkit/ui/screens/contacts/EditContactScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/contacts/EditContactScreen.kt @@ -97,16 +97,25 @@ private fun Content( onDismissAddTagSheet: () -> Unit, onSaveTag: (String) -> Unit, ) { - ScreenColumn { + val topBar = @Composable { modifier: Modifier -> AppTopBar( titleText = stringResource(R.string.contacts__edit_contact_title), onBackClick = onBackClick, actions = { DrawerNavIcon() }, + modifier = modifier, ) + } + ScreenColumn { when { - uiState.isLoading -> LoadingState() - uiState.isMissing -> EmptyState(onRetryClick = onRetryClick) + uiState.isLoading -> { + topBar(Modifier) + LoadingState() + } + uiState.isMissing -> { + topBar(Modifier) + EmptyState(onRetryClick = onRetryClick) + } else -> ProfileEditForm( name = uiState.name, onNameChange = onNameChange, @@ -123,6 +132,7 @@ private fun Content( onSave = onSave, onCancel = onBackClick, isSaveEnabled = uiState.name.isNotBlank() && !uiState.isSaving, + topBar = topBar, avatarContent = { ContactEditAvatar(imageUrl = uiState.imageUrl) }, diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/CreateProfileScreen.kt b/app/src/main/java/to/bitkit/ui/screens/profile/CreateProfileScreen.kt index a1721cb827..df88a63246 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/CreateProfileScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/CreateProfileScreen.kt @@ -10,17 +10,16 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -28,25 +27,20 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import to.bitkit.R import to.bitkit.ui.components.BodyM -import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.GradientCircularProgressIndicator import to.bitkit.ui.components.PrimaryButton -import to.bitkit.ui.components.Text13Up -import to.bitkit.ui.components.TextInput +import to.bitkit.ui.components.ProfileEditHeader import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.theme.AppTextFieldDefaults -import to.bitkit.ui.theme.AppTextStyles import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -115,48 +109,26 @@ private fun Content( if (uiState.isLoading) { LoadingState(text = stringResource(R.string.profile__deriving_keys)) } else { + val isNameInitiallyEmpty = remember { uiState.name.isEmpty() } Column( - horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) - .padding(horizontal = 32.dp) + .padding(horizontal = 16.dp) ) { - VerticalSpacer(32.dp) - - AvatarPickerButton( - avatarUri = uiState.avatarUri, - onClick = launchPhotoPicker, - modifier = Modifier.testTag("CreateProfileAvatar"), - ) - - VerticalSpacer(32.dp) - - TextInput( - value = uiState.name, - onValueChange = onNameChange, - placeholder = stringResource(R.string.profile__edit_name_placeholder), - singleLine = true, - textStyle = AppTextStyles.Display.copy(textAlign = TextAlign.Center), - colors = AppTextFieldDefaults.transparent, - modifier = Modifier - .fillMaxWidth() - .testTag("CreateProfileUsername"), - ) - - VerticalSpacer(16.dp) - HorizontalDivider() - VerticalSpacer(16.dp) - - Text13Up( - text = stringResource(R.string.profile__your_pubky), - color = Colors.White64, - ) - VerticalSpacer(8.dp) - BodyMSB( - text = uiState.derivedPublicKey ?: "...", - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), + ProfileEditHeader( + name = uiState.name, + onNameChange = onNameChange, + publicKey = uiState.derivedPublicKey ?: "...", + nameTestTag = "CreateProfileUsername", + autoFocusName = isNameInitiallyEmpty, + avatarContent = { + AvatarPickerButton( + avatarUri = uiState.avatarUri, + onClick = launchPhotoPicker, + modifier = Modifier.testTag("CreateProfileAvatar"), + ) + }, ) FillHeight() @@ -167,7 +139,7 @@ private fun Content( onClick = onSave, enabled = uiState.name.isNotBlank() && !uiState.isSaving, isLoading = uiState.isSaving, - modifier = Modifier.testTag("CreateProfileSave"), + modifier = Modifier.testTag("CreateProfileSave") ) VerticalSpacer(16.dp) } @@ -186,7 +158,7 @@ private fun AvatarPickerButton( modifier = modifier .size(96.dp) .clip(CircleShape) - .background(Colors.Gray5) + .background(Colors.Gray6) .clickable(onClick = onClick), ) { if (avatarUri != null) { @@ -198,10 +170,10 @@ private fun AvatarPickerButton( ) } else { Icon( - painter = painterResource(R.drawable.ic_user_square), + painter = painterResource(R.drawable.ic_image_square), contentDescription = null, tint = Colors.White32, - modifier = Modifier.size(40.dp), + modifier = Modifier.size(32.dp), ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/EditProfileScreen.kt b/app/src/main/java/to/bitkit/ui/screens/profile/EditProfileScreen.kt index 0d0c5a5e69..1f20eecbb4 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/EditProfileScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/EditProfileScreen.kt @@ -128,14 +128,18 @@ private fun Content( } } - ScreenColumn { + val topBar = @Composable { modifier: Modifier -> AppTopBar( titleText = stringResource(R.string.profile__edit_nav_title), onBackClick = onBackClick, actions = { DrawerNavIcon() }, + modifier = modifier, ) + } + ScreenColumn { if (uiState.isLoading) { + topBar(Modifier) Box( contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize() @@ -159,6 +163,7 @@ private fun Content( onSave = onSave, onCancel = onBackClick, isSaveEnabled = uiState.name.isNotBlank() && !uiState.isSaving, + topBar = topBar, avatarContent = { AvatarSection( imageUrl = uiState.imageUrl, @@ -224,7 +229,7 @@ private fun AvatarSection( modifier = Modifier .size(96.dp) .clip(CircleShape) - .background(Colors.Gray5) + .background(Colors.Gray6) .testTag("EditProfileAvatar") .clickable(onClick = onClick) ) { @@ -237,10 +242,10 @@ private fun AvatarSection( ) imageUrl != null -> PubkyImage(uri = imageUrl, size = 96.dp) else -> Icon( - painter = painterResource(R.drawable.ic_user_square), + painter = painterResource(R.drawable.ic_image_square), contentDescription = null, tint = Colors.White32, - modifier = Modifier.size(40.dp) + modifier = Modifier.size(32.dp) ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/PayContactsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/profile/PayContactsScreen.kt index ee6baa0de5..6095a516cf 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/PayContactsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/PayContactsScreen.kt @@ -1,12 +1,15 @@ package to.bitkit.ui.screens.profile import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -62,36 +65,41 @@ private fun Content( actions = { DrawerNavIcon() }, ) - Column( - modifier = Modifier.padding(horizontal = 32.dp) + Box( + contentAlignment = Alignment.BottomCenter, + modifier = Modifier + .fillMaxWidth() + .weight(1f) ) { Image( painter = painterResource(R.drawable.coin_stack), contentDescription = null, - modifier = Modifier - .fillMaxWidth() - .weight(1f) + modifier = Modifier.size(279.dp) ) - + } + VerticalSpacer(16.dp) + Column(modifier = Modifier.padding(horizontal = 32.dp)) { Display( text = stringResource(R.string.profile__pay_contacts_headline) .withAccent(accentColor = Colors.PubkyGreen), color = Colors.White, ) - VerticalSpacer(16.dp) + VerticalSpacer(8.dp) BodyM( text = stringResource(R.string.profile__pay_contacts_description), color = Colors.White64, ) - VerticalSpacer(32.dp) - PrimaryButton( - text = stringResource(R.string.common__continue), - onClick = onContinue, - enabled = !uiState.isLoading, - modifier = Modifier.testTag("PayContactsContinue") - ) - VerticalSpacer(16.dp) } + VerticalSpacer(32.dp) + PrimaryButton( + text = stringResource(R.string.common__continue), + onClick = onContinue, + enabled = !uiState.isLoading, + modifier = Modifier + .padding(horizontal = 16.dp) + .testTag("PayContactsContinue") + ) + VerticalSpacer(16.dp) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/ProfileScreen.kt b/app/src/main/java/to/bitkit/ui/screens/profile/ProfileScreen.kt index 38e0e65c9e..c30ee8967c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/ProfileScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/ProfileScreen.kt @@ -1,5 +1,10 @@ package to.bitkit.ui.screens.profile +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -14,17 +19,26 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -34,15 +48,19 @@ import to.bitkit.models.PubkyProfileLink import to.bitkit.ui.components.ActionButton import to.bitkit.ui.components.AddTagSheet import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.BodyS +import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.CenteredProfileHeader import to.bitkit.ui.components.GradientCircularProgressIndicator import to.bitkit.ui.components.LinkRow +import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.PubkyImage import to.bitkit.ui.components.QrCodeImage import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TagButton import to.bitkit.ui.components.Text13Up +import to.bitkit.ui.components.TopBarSpacer import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.AppTopBar @@ -50,9 +68,11 @@ import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.rememberDebouncedClick +import to.bitkit.ui.shared.util.screen import to.bitkit.ui.shared.util.shareText import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.theme.TopBarGradient @Composable fun ProfileScreen( @@ -85,6 +105,7 @@ fun ProfileScreen( onRemoveTag = { viewModel.removeTag(it) }, onDismissAddTagSheet = { viewModel.dismissAddTagSheet() }, onSaveTag = { viewModel.addTag(it) }, + onDismissCopiedPopup = { viewModel.dismissCopiedPopup() }, ) } @@ -103,19 +124,30 @@ private fun Content( onRemoveTag: (String) -> Unit, onDismissAddTagSheet: () -> Unit, onSaveTag: (String) -> Unit, + onDismissCopiedPopup: () -> Unit, ) { val currentProfile = uiState.profile - - ScreenColumn { + val topBar = @Composable { modifier: Modifier -> AppTopBar( titleText = stringResource(R.string.profile__nav_title), onBackClick = onBackClick, actions = { DrawerNavIcon() }, + modifier = modifier, ) + } - when { - uiState.isLoading && currentProfile == null -> LoadingState() - currentProfile != null -> ProfileBody( + if (currentProfile == null) { + ScreenColumn { + topBar(Modifier) + if (uiState.isLoading) { + LoadingState() + } else { + EmptyState(onClickRetry = onClickRetry, onClickSignOut = onClickSignOut) + } + } + } else { + Box(modifier = Modifier.screen()) { + ProfileBody( profile = currentProfile, onClickEdit = onClickEdit, onClickCopy = onClickCopy, @@ -123,7 +155,12 @@ private fun Content( onClickAddTag = onClickAddTag, onRemoveTag = onRemoveTag, ) - else -> EmptyState(onClickRetry = onClickRetry, onClickSignOut = onClickSignOut) + topBar(Modifier.background(TopBarGradient)) + CopiedPopup( + publicKey = uiState.copiedPublicKey, + onClick = onDismissCopiedPopup, + modifier = Modifier.align(Alignment.Center) + ) } } @@ -161,6 +198,7 @@ private fun ProfileBody( .verticalScroll(rememberScrollState()) .padding(horizontal = 16.dp) ) { + TopBarSpacer() VerticalSpacer(24.dp) CenteredProfileHeader( @@ -172,17 +210,18 @@ private fun ProfileBody( notesTestTag = "ProfileViewNotes", ) - VerticalSpacer(24.dp) + VerticalSpacer(16.dp) Box( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxWidth() + .padding(horizontal = 32.dp) .clickableAlpha(onClick = onClickCopy) ) { QrCodeImage( content = profile.publicKey, - modifier = Modifier.size(279.dp), + modifier = Modifier.fillMaxWidth(), testTag = "ProfileQRCode", ) if (profile.imageUrl != null) { @@ -200,7 +239,7 @@ private fun ProfileBody( } } - VerticalSpacer(24.dp) + VerticalSpacer(16.dp) Row( horizontalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterHorizontally), @@ -208,12 +247,12 @@ private fun ProfileBody( ) { ActionButton( onClick = onClickEdit, - iconRes = R.drawable.ic_edit, + iconRes = R.drawable.ic_pencil, modifier = Modifier.testTag("ProfileEdit") ) ActionButton( onClick = onClickCopy, - iconRes = R.drawable.ic_copy, + iconRes = R.drawable.ic_copy_simple, modifier = Modifier.testTag("ProfileCopy") ) ActionButton( @@ -224,11 +263,10 @@ private fun ProfileBody( } VerticalSpacer(32.dp) + HorizontalDivider() - if (profile.links.isNotEmpty()) { - profile.links.forEachIndexed { index, link -> - LinkRow(label = link.label, value = link.url, linkIndex = index) - } + profile.links.forEachIndexed { index, link -> + LinkRow(label = link.label, value = link.url, linkIndex = index) } VerticalSpacer(16.dp) @@ -240,25 +278,37 @@ private fun ProfileBody( .testTag("ProfileViewTagsHeader") ) VerticalSpacer(8.dp) - @OptIn(ExperimentalLayoutApi::class) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth() - ) { - profile.tags.forEach { tag -> - TagButton( - text = tag, - onClick = { onRemoveTag(tag) }, - accessibilityLabel = stringResource(R.string.common__remove_tag, tag), - displayIconClose = true, - ) + if (profile.tags.isNotEmpty()) { + @OptIn(ExperimentalLayoutApi::class) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth() + ) { + profile.tags.forEach { tag -> + TagButton( + text = tag, + onClick = { onRemoveTag(tag) }, + accessibilityLabel = stringResource(R.string.common__remove_tag, tag), + displayIconClose = true, + ) + } } - TagButton( + VerticalSpacer(8.dp) + } + Row(modifier = Modifier.fillMaxWidth()) { + PrimaryButton( text = stringResource(R.string.profile__add_tag), onClick = onClickAddTag, - icon = painterResource(R.drawable.ic_tag), - displayIconClose = true, + size = ButtonSize.Small, + fullWidth = false, + icon = { + Icon( + painter = painterResource(R.drawable.ic_tag), + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + }, modifier = Modifier.testTag("ProfileAddTag") ) } @@ -267,6 +317,49 @@ private fun ProfileBody( } } +@Composable +private fun CopiedPopup( + publicKey: String?, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + var lastPublicKey by remember { mutableStateOf("") } + LaunchedEffect(publicKey) { + if (publicKey != null) lastPublicKey = publicKey + } + + AnimatedVisibility( + visible = publicKey != null, + enter = fadeIn() + scaleIn(initialScale = 0.95f), + exit = fadeOut() + scaleOut(targetScale = 0.95f), + modifier = modifier + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(horizontal = 32.dp) + .shadow(elevation = 25.dp, shape = MaterialTheme.shapes.medium, spotColor = Colors.Black) + .clip(MaterialTheme.shapes.medium) + .background(Colors.Gray6) + .clickableAlpha(onClick = onClick) + .padding(32.dp) + .testTag("ProfilePubkyCopiedToast") + ) { + BodyMSB( + text = stringResource(R.string.profile__pubky_copied), + color = Colors.Brand, + textAlign = TextAlign.Center, + ) + BodyS( + text = lastPublicKey, + color = Colors.White, + textAlign = TextAlign.Center, + ) + } + } +} + @Composable private fun LoadingState() { Box( @@ -334,6 +427,7 @@ private fun Preview() { onRemoveTag = {}, onDismissAddTagSheet = {}, onSaveTag = {}, + onDismissCopiedPopup = {}, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/ProfileViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/profile/ProfileViewModel.kt index 280edad106..2140ed08d2 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/ProfileViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/ProfileViewModel.kt @@ -6,6 +6,8 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -26,6 +28,7 @@ import to.bitkit.repositories.PubkyRepo import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.utils.Logger import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds @HiltViewModel class ProfileViewModel @Inject constructor( @@ -35,18 +38,22 @@ class ProfileViewModel @Inject constructor( ) : ViewModel() { companion object { private const val TAG = "ProfileViewModel" + private val COPIED_POPUP_DURATION = 3.seconds } private val _showSignOutDialog = MutableStateFlow(false) private val _isSigningOut = MutableStateFlow(false) private val _showAddTagSheet = MutableStateFlow(false) + private val _copiedPublicKey = MutableStateFlow(null) private val tagUpdateMutex = Mutex() + private var hideCopiedPopupJob: Job? = null private val controls = combine( _showSignOutDialog, _isSigningOut, _showAddTagSheet, - ) { showSignOutDialog, isSigningOut, showAddTagSheet -> - ProfileControls(showSignOutDialog, isSigningOut, showAddTagSheet) + _copiedPublicKey, + ) { showSignOutDialog, isSigningOut, showAddTagSheet, copiedPublicKey -> + ProfileControls(showSignOutDialog, isSigningOut, showAddTagSheet, copiedPublicKey) } val uiState: StateFlow = combine( @@ -62,6 +69,7 @@ class ProfileViewModel @Inject constructor( showSignOutDialog = controls.showSignOutDialog, isSigningOut = controls.isSigningOut, showAddTagSheet = controls.showAddTagSheet, + copiedPublicKey = controls.copiedPublicKey, ) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), ProfileUiState()) @@ -140,15 +148,19 @@ class ProfileViewModel @Inject constructor( fun copyPublicKey() { val pk = pubkyRepo.publicKey.value ?: return context.setClipboardText(pk, context.getString(R.string.profile__public_key)) - viewModelScope.launch { - ToastEventBus.send( - type = Toast.ToastType.SUCCESS, - title = context.getString(R.string.common__copied), - testTag = "ProfilePubkyCopiedToast", - ) + _copiedPublicKey.update { pk } + hideCopiedPopupJob?.cancel() + hideCopiedPopupJob = viewModelScope.launch { + delay(COPIED_POPUP_DURATION) + _copiedPublicKey.update { null } } } + fun dismissCopiedPopup() { + hideCopiedPopupJob?.cancel() + _copiedPublicKey.update { null } + } + private fun updateTags( transform: (List) -> List, onSuccess: () -> Unit = {}, @@ -191,12 +203,14 @@ data class ProfileUiState( val showSignOutDialog: Boolean = false, val isSigningOut: Boolean = false, val showAddTagSheet: Boolean = false, + val copiedPublicKey: String? = null, ) private data class ProfileControls( val showSignOutDialog: Boolean, val isSigningOut: Boolean, val showAddTagSheet: Boolean, + val copiedPublicKey: String?, ) sealed interface ProfileEffect { diff --git a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyChoiceScreen.kt b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyChoiceScreen.kt index 854541911d..04c84e6a2c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/profile/PubkyChoiceScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/profile/PubkyChoiceScreen.kt @@ -9,11 +9,13 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon @@ -51,12 +53,13 @@ import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent private const val PUBKY_RING_PLAY_STORE_URL = "https://play.google.com/store/apps/details?id=to.pubky.ring" -private const val BG_IMAGE_WIDTH_FRACTION = 0.83f -private const val TAG_OFFSET_X = -0.179f -private const val TAG_OFFSET_Y = 0.13f -private const val KEYRING_OFFSET_X = 0.341f -private const val KEYRING_OFFSET_Y = 0.06f -private const val TAG_ALPHA = 0.6f + +private const val TAG_WIDTH_FRACTION = 0.736f +private const val TAG_OFFSET_X = -0.197f +private const val TAG_OFFSET_Y = 0.067f +private const val KEYRING_WIDTH_FRACTION = 0.83f +private const val KEYRING_OFFSET_X = 0.335f +private const val KEYRING_OFFSET_Y = -0.109f private const val KEYRING_ALPHA = 0.9f @Composable @@ -119,7 +122,7 @@ private fun Content( ) { Box( modifier = Modifier - .screen() + .screen(insets = WindowInsets.statusBars) .clipToBounds() ) { BoxWithConstraints(modifier = Modifier.fillMaxSize()) { @@ -128,10 +131,9 @@ private fun Content( contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier - .fillMaxWidth(BG_IMAGE_WIDTH_FRACTION) - .align(Alignment.Center) - .offset(x = maxWidth * TAG_OFFSET_X, y = maxHeight * TAG_OFFSET_Y) - .alpha(TAG_ALPHA) + .fillMaxWidth(TAG_WIDTH_FRACTION) + .align(Alignment.BottomStart) + .offset(x = maxWidth * TAG_OFFSET_X, y = maxWidth * TAG_OFFSET_Y) ) Image( @@ -139,9 +141,9 @@ private fun Content( contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier - .fillMaxWidth(BG_IMAGE_WIDTH_FRACTION) - .align(Alignment.Center) - .offset(x = maxWidth * KEYRING_OFFSET_X, y = maxHeight * KEYRING_OFFSET_Y) + .fillMaxWidth(KEYRING_WIDTH_FRACTION) + .align(Alignment.BottomStart) + .offset(x = maxWidth * KEYRING_OFFSET_X, y = maxWidth * KEYRING_OFFSET_Y) .alpha(KEYRING_ALPHA) ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt index 6e6e8f41e3..0873bad062 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt @@ -136,6 +136,7 @@ import to.bitkit.ui.Routes import to.bitkit.ui.components.ActivityBanner import to.bitkit.ui.components.AppStatus import to.bitkit.ui.components.BalanceHeaderView +import to.bitkit.ui.components.CaptionB import to.bitkit.ui.components.EmptyStateView import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.FillWidth @@ -1420,6 +1421,7 @@ private fun ProfileButton( displayImageUri: String?, onClick: () -> Unit, ) { + val name = displayName ?: stringResource(R.string.profile__your_name) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(16.dp), @@ -1438,19 +1440,17 @@ private fun ProfileButton( modifier = Modifier .size(32.dp) .clip(CircleShape) - .background(Colors.Gray4) + .background(Colors.Gray6) ) { - Icon( - painter = painterResource(R.drawable.ic_user_square), - contentDescription = null, - tint = Colors.White32, - modifier = Modifier.size(16.dp) + CaptionB( + text = name.firstOrNull()?.uppercase().orEmpty(), + color = Colors.White, ) } } Title( - text = displayName ?: stringResource(R.string.profile__your_name), + text = name, maxLines = 1, ) } diff --git a/app/src/main/res/drawable/ic_copy_simple.xml b/app/src/main/res/drawable/ic_copy_simple.xml new file mode 100644 index 0000000000..c5781d59f8 --- /dev/null +++ b/app/src/main/res/drawable/ic_copy_simple.xml @@ -0,0 +1,22 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_pencil.xml b/app/src/main/res/drawable/ic_pencil.xml new file mode 100644 index 0000000000..f9f8eb67be --- /dev/null +++ b/app/src/main/res/drawable/ic_pencil.xml @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7af882dd27..eaeefe3282 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -659,10 +659,10 @@ This Bitkit claim is not supported. Failed to read selected image Create profile with Bitkit - Create a new pubky and profile in Bitkit, or import an existing profile with Pubky Ring. + Create a new pubky and profile in Bitkit. Import with Pubky Ring Loading your profile… - Join the\n<accent>pubky web</accent> + Enter the\n<accent>freedom web</accent> Waiting for Pubky Ring… Failed to create profile Create Profile @@ -696,6 +696,7 @@ Wallet is still starting. Try again in a moment. Let your\ncontacts\n<accent>pay you</accent> Pay Contacts + pubky copied to clipboard Public Key Scan to add {name} Restore Profile diff --git a/changelog.d/next/1332.fixed.md b/changelog.d/next/1332.fixed.md new file mode 100644 index 0000000000..f4e028a5f1 --- /dev/null +++ b/changelog.d/next/1332.fixed.md @@ -0,0 +1 @@ +The Create Profile, Edit Profile and Profile screens now match the design, with the edit form scrolling under the Cancel and Save buttons and tags managed from Edit Profile.