diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/profile/ProfileScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/profile/ProfileScreen.kt
index 38910a0..6587f13 100644
--- a/android/app/src/main/java/ru/daemonlord/messenger/ui/profile/ProfileScreen.kt
+++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/profile/ProfileScreen.kt
@@ -1,11 +1,13 @@
package ru.daemonlord.messenger.ui.profile
import android.content.Context
+import android.content.Intent
import android.graphics.Bitmap
import android.graphics.ImageDecoder
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
+import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
@@ -15,6 +17,7 @@ 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.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
@@ -33,10 +36,15 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.AddAPhoto
+import androidx.compose.material.icons.filled.AlternateEmail
+import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
@@ -58,7 +66,9 @@ import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.IntSize
@@ -122,7 +132,11 @@ fun ProfileScreen(
}
val context = LocalContext.current
+ val clipboardManager = LocalClipboardManager.current
val isTabletLayout = LocalConfiguration.current.screenWidthDp >= 840
+ val profileDisplayName = if (name.isBlank()) stringResource(id = R.string.profile_user_fallback) else name
+ val usernameLabel = username.trim().takeIf { it.isNotBlank() }?.let { "@$it" }
+ val emailLabel = profile?.email?.takeIf { it.isNotBlank() }
val pickAvatarLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent(),
) { uri ->
@@ -192,18 +206,20 @@ fun ProfileScreen(
}
Text(
- text = if (name.isBlank()) stringResource(id = R.string.profile_user_fallback) else name,
+ text = profileDisplayName,
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer,
fontWeight = FontWeight.SemiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
- Text(
- stringResource(id = R.string.chat_status_online),
- color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f),
- style = MaterialTheme.typography.bodyLarge,
- )
+ usernameLabel?.let {
+ Text(
+ text = it,
+ color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.82f),
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ }
Row(
modifier = Modifier.fillMaxWidth(),
@@ -247,13 +263,139 @@ fun ProfileScreen(
verticalArrangement = Arrangement.spacedBy(14.dp),
) {
val notSet = stringResource(id = R.string.profile_not_set)
- ProfileInfoRow(stringResource(id = R.string.auth_label_email), profile?.email.orEmpty())
- ProfileInfoRow(stringResource(id = R.string.profile_bio), bio.ifBlank { notSet })
- ProfileInfoRow(
- stringResource(id = R.string.auth_label_username),
- if (username.isBlank()) notSet else "@$username",
+ Text(
+ text = stringResource(id = R.string.profile_about_section),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
)
- ProfileInfoRow(stringResource(id = R.string.auth_label_name), name.ifBlank { notSet })
+ ProfileInfoRow(
+ label = stringResource(id = R.string.auth_label_name),
+ value = profileDisplayName,
+ )
+ ProfileInfoRow(
+ label = stringResource(id = R.string.auth_label_username),
+ value = usernameLabel ?: notSet,
+ actions = {
+ usernameLabel?.let { handle ->
+ ProfileInlineAction(
+ icon = Icons.Filled.ContentCopy,
+ label = stringResource(id = R.string.profile_copy_username),
+ ) {
+ clipboardManager.setText(AnnotatedString(handle))
+ Toast.makeText(
+ context,
+ context.getString(R.string.profile_username_copied),
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ ProfileInlineAction(
+ icon = Icons.Filled.Share,
+ label = stringResource(id = R.string.profile_share_username),
+ ) {
+ val shareIntent = Intent(Intent.ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(Intent.EXTRA_TEXT, handle)
+ }
+ context.startActivity(
+ Intent.createChooser(
+ shareIntent,
+ context.getString(R.string.profile_share_username),
+ ),
+ )
+ }
+ }
+ },
+ )
+ ProfileInfoRow(
+ label = stringResource(id = R.string.auth_label_email),
+ value = emailLabel ?: notSet,
+ actions = {
+ emailLabel?.let { email ->
+ ProfileInlineAction(
+ icon = Icons.Filled.ContentCopy,
+ label = stringResource(id = R.string.profile_copy_email),
+ ) {
+ clipboardManager.setText(AnnotatedString(email))
+ Toast.makeText(
+ context,
+ context.getString(R.string.profile_email_copied),
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ }
+ },
+ )
+ ProfileInfoRow(
+ label = stringResource(id = R.string.profile_bio),
+ value = bio.ifBlank { notSet },
+ )
+ }
+ }
+
+ Surface(
+ color = MaterialTheme.colorScheme.surfaceContainer,
+ shape = RoundedCornerShape(22.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .offset(y = (-10).dp),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ Text(
+ text = stringResource(id = R.string.profile_actions_section),
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.SemiBold,
+ )
+ ProfileActionRow(
+ icon = Icons.Filled.Edit,
+ title = stringResource(id = R.string.profile_edit),
+ subtitle = stringResource(id = R.string.profile_edit_subtitle),
+ ) {
+ editMode = true
+ }
+ ProfileActionRow(
+ icon = Icons.Filled.AddAPhoto,
+ title = stringResource(id = R.string.profile_choose_photo),
+ subtitle = stringResource(id = R.string.profile_photo_subtitle),
+ ) {
+ pickAvatarLauncher.launch("image/*")
+ }
+ if (usernameLabel != null) {
+ ProfileActionRow(
+ icon = Icons.Filled.AlternateEmail,
+ title = stringResource(id = R.string.profile_share_username),
+ subtitle = usernameLabel,
+ ) {
+ val shareIntent = Intent(Intent.ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(Intent.EXTRA_TEXT, usernameLabel)
+ }
+ context.startActivity(
+ Intent.createChooser(
+ shareIntent,
+ context.getString(R.string.profile_share_username),
+ ),
+ )
+ }
+ }
+ if (emailLabel != null) {
+ ProfileActionRow(
+ icon = Icons.Filled.Email,
+ title = stringResource(id = R.string.profile_copy_email),
+ subtitle = emailLabel,
+ ) {
+ clipboardManager.setText(AnnotatedString(emailLabel))
+ Toast.makeText(
+ context,
+ context.getString(R.string.profile_email_copied),
+ Toast.LENGTH_SHORT,
+ ).show()
+ }
+ }
}
}
}
@@ -304,6 +446,11 @@ fun ProfileScreen(
label = { Text(stringResource(id = R.string.profile_avatar_url)) },
modifier = Modifier.fillMaxWidth(),
)
+ Text(
+ text = stringResource(id = R.string.profile_edit_hint),
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
TextButton(onClick = { editMode = false }, modifier = Modifier.weight(1f)) {
Text(stringResource(id = R.string.common_cancel))
@@ -392,9 +539,97 @@ private fun HeroActionButton(
@Composable
private fun ProfileInfoRow(label: String, value: String) {
- Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
- Text(text = value, style = MaterialTheme.typography.titleLarge)
- Text(text = label, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant)
+ ProfileInfoRow(label = label, value = value, actions = {})
+}
+
+@Composable
+private fun ProfileInfoRow(
+ label: String,
+ value: String,
+ actions: @Composable RowScope.() -> Unit,
+) {
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.Top,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ Column(
+ modifier = Modifier.weight(1f),
+ verticalArrangement = Arrangement.spacedBy(2.dp),
+ ) {
+ Text(text = value, style = MaterialTheme.typography.titleLarge)
+ Text(
+ text = label,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ actions()
+ }
+}
+
+@Composable
+private fun ProfileInlineAction(
+ icon: androidx.compose.ui.graphics.vector.ImageVector,
+ label: String,
+ onClick: () -> Unit,
+) {
+ IconButton(onClick = onClick) {
+ Icon(
+ imageVector = icon,
+ contentDescription = label,
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
+}
+
+@Composable
+private fun ProfileActionRow(
+ icon: androidx.compose.ui.graphics.vector.ImageVector,
+ title: String,
+ subtitle: String,
+ onClick: () -> Unit,
+) {
+ Surface(
+ onClick = onClick,
+ shape = RoundedCornerShape(18.dp),
+ color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f),
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 14.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ Box(
+ modifier = Modifier
+ .size(42.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ imageVector = icon,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary,
+ )
+ }
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.SemiBold,
+ )
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
}
}
diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml
index dd5371e..77380f1 100644
--- a/android/app/src/main/res/values-ru/strings.xml
+++ b/android/app/src/main/res/values-ru/strings.xml
@@ -100,6 +100,16 @@
Не указано
Редактировать профиль
URL аватара
+ Профиль
+ Действия
+ Скопировать юзернейм
+ Поделиться юзернеймом
+ Юзернейм скопирован
+ Скопировать email
+ Email скопирован
+ Изменить имя, юзернейм, описание и аватар.
+ Выбрать и обрезать новое фото профиля.
+ Изменения сохраняются в аккаунт и используются во всём приложении.
Обрезать аватар
Предпросмотр обрезки аватара
Использовать
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 973a9f4..8b729d9 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -100,6 +100,16 @@
Not set
Edit profile
Avatar URL
+ Profile
+ Actions
+ Copy username
+ Share username
+ Username copied
+ Copy email
+ Email copied
+ Update name, username, bio and avatar.
+ Choose and crop a new profile photo.
+ Changes are saved to your account and used across the app.
Crop avatar
Avatar crop preview
Use