diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/contacts/ContactsScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/contacts/ContactsScreen.kt
index 375a9ed..c7d587f 100644
--- a/android/app/src/main/java/ru/daemonlord/messenger/ui/contacts/ContactsScreen.kt
+++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/contacts/ContactsScreen.kt
@@ -1,34 +1,48 @@
package ru.daemonlord.messenger.ui.contacts
+import androidx.compose.foundation.background
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.WindowInsets
+import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Email
+import androidx.compose.material.icons.filled.PersonAdd
+import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
@@ -119,27 +133,83 @@ private fun ContactsScreen(
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
- OutlinedTextField(
- value = state.query,
- onValueChange = onQueryChanged,
+ Surface(
modifier = Modifier.fillMaxWidth(),
- singleLine = true,
- label = { Text(stringResource(id = R.string.contacts_search_label)) },
- )
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- verticalAlignment = Alignment.CenterVertically,
+ shape = CircleShape,
+ color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.42f),
) {
OutlinedTextField(
- value = state.addByEmail,
- onValueChange = onAddByEmailChanged,
- modifier = Modifier.weight(1f),
+ value = state.query,
+ onValueChange = onQueryChanged,
+ modifier = Modifier.fillMaxWidth(),
singleLine = true,
- label = { Text(stringResource(id = R.string.contacts_add_by_email_label)) },
+ leadingIcon = {
+ Icon(
+ imageVector = Icons.Filled.Search,
+ contentDescription = null,
+ )
+ },
+ placeholder = { Text(stringResource(id = R.string.contacts_search_label)) },
+ shape = CircleShape,
+ colors = androidx.compose.material3.OutlinedTextFieldDefaults.colors(
+ unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0f),
+ focusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0f),
+ unfocusedContainerColor = androidx.compose.ui.graphics.Color.Transparent,
+ focusedContainerColor = androidx.compose.ui.graphics.Color.Transparent,
+ ),
)
- Button(onClick = onAddContactByEmail) {
- Text(stringResource(id = R.string.common_create))
+ }
+
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(24.dp),
+ color = MaterialTheme.colorScheme.surface.copy(alpha = 0.68f),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(14.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ Text(
+ text = stringResource(id = R.string.contacts_quick_actions),
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.SemiBold,
+ )
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ ) {
+ ContactActionCard(
+ modifier = Modifier.weight(1f),
+ icon = Icons.Filled.PersonAdd,
+ title = stringResource(id = R.string.contacts_action_find_people),
+ subtitle = stringResource(id = R.string.contacts_action_find_people_subtitle),
+ )
+ ContactActionCard(
+ modifier = Modifier.weight(1f),
+ icon = Icons.Filled.Email,
+ title = stringResource(id = R.string.contacts_action_add_email),
+ subtitle = stringResource(id = R.string.contacts_action_add_email_subtitle),
+ )
+ }
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ OutlinedTextField(
+ value = state.addByEmail,
+ onValueChange = onAddByEmailChanged,
+ modifier = Modifier.weight(1f),
+ singleLine = true,
+ label = { Text(stringResource(id = R.string.contacts_add_by_email_label)) },
+ shape = RoundedCornerShape(20.dp),
+ )
+ Button(onClick = onAddContactByEmail) {
+ Text(stringResource(id = R.string.common_create))
+ }
+ }
}
}
@@ -173,70 +243,32 @@ private fun ContactsScreen(
) {
if (state.isSearchingUsers || state.query.trim().length >= 2) {
item(key = "search_header") {
- Text(
- text = stringResource(id = R.string.contacts_search_results),
- style = MaterialTheme.typography.titleSmall,
- fontWeight = FontWeight.SemiBold,
- )
+ ContactsSectionHeader(title = stringResource(id = R.string.contacts_search_results))
}
items(state.searchResults, key = { "search_${it.id}" }) { user ->
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = user.name,
- fontWeight = FontWeight.SemiBold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- Text(
- text = user.username?.let { "@$it" } ?: "id: ${user.id}",
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
- }
- OutlinedButton(onClick = { onAddContact(user.id) }) {
- Text(stringResource(id = R.string.common_create))
- }
- }
+ ContactRowCard(
+ name = user.name,
+ subtitle = user.username?.let { "@$it" } ?: "id: ${user.id}",
+ actionLabel = stringResource(id = R.string.common_create),
+ onAction = { onAddContact(user.id) },
+ )
}
}
item(key = "contacts_header") {
- Text(
- text = stringResource(id = R.string.contacts_my_contacts),
- style = MaterialTheme.typography.titleSmall,
- fontWeight = FontWeight.SemiBold,
+ ContactsSectionHeader(
+ title = stringResource(id = R.string.contacts_my_contacts),
modifier = Modifier.padding(top = 4.dp),
)
}
items(state.contacts, key = { "contact_${it.id}" }) { contact ->
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.SpaceBetween,
- verticalAlignment = Alignment.CenterVertically,
- ) {
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = contact.name,
- fontWeight = FontWeight.SemiBold,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- Text(
- text = contact.username?.let { "@$it" } ?: stringResource(id = R.string.contacts_last_seen_recently),
- style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
- )
- }
- OutlinedButton(onClick = { onRemoveContact(contact.id) }) {
- Text(stringResource(id = R.string.contacts_remove))
- }
- }
+ ContactRowCard(
+ name = contact.name,
+ subtitle = contact.username?.let { "@$it" } ?: stringResource(id = R.string.contacts_last_seen_recently),
+ actionLabel = stringResource(id = R.string.contacts_remove),
+ onAction = { onRemoveContact(contact.id) },
+ )
}
if (state.contacts.isEmpty()) {
@@ -255,3 +287,133 @@ private fun ContactsScreen(
}
}
}
+
+@Composable
+private fun ContactsSectionHeader(
+ title: String,
+ modifier: Modifier = Modifier,
+) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.SemiBold,
+ modifier = modifier,
+ )
+}
+
+@Composable
+private fun ContactActionCard(
+ modifier: Modifier = Modifier,
+ icon: androidx.compose.ui.graphics.vector.ImageVector,
+ title: String,
+ subtitle: String,
+) {
+ Surface(
+ modifier = modifier,
+ shape = RoundedCornerShape(20.dp),
+ color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.34f),
+ ) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(14.dp),
+ verticalArrangement = Arrangement.spacedBy(8.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,
+ )
+ }
+ Text(
+ text = title,
+ style = MaterialTheme.typography.titleSmall,
+ fontWeight = FontWeight.SemiBold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+}
+
+@Composable
+private fun ContactRowCard(
+ name: String,
+ subtitle: String,
+ actionLabel: String,
+ onAction: () -> Unit,
+) {
+ Surface(
+ modifier = Modifier.fillMaxWidth(),
+ shape = RoundedCornerShape(22.dp),
+ color = MaterialTheme.colorScheme.surface.copy(alpha = 0.7f),
+ ) {
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 14.dp, vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ ContactAvatar(name = name)
+ Column(modifier = Modifier.weight(1f)) {
+ Text(
+ text = name,
+ fontWeight = FontWeight.SemiBold,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Text(
+ text = subtitle,
+ style = MaterialTheme.typography.bodySmall,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ OutlinedButton(onClick = onAction) {
+ Text(actionLabel)
+ }
+ }
+ }
+}
+
+@Composable
+private fun ContactAvatar(name: String) {
+ val initials = remember(name) {
+ name.trim()
+ .split(Regex("\\s+"))
+ .filter { it.isNotBlank() }
+ .take(2)
+ .joinToString("") { it.first().uppercase() }
+ .ifBlank { "?" }
+ }
+ Box(
+ modifier = Modifier
+ .size(48.dp)
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.18f)),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = initials,
+ style = MaterialTheme.typography.titleMedium,
+ fontWeight = FontWeight.Bold,
+ color = MaterialTheme.colorScheme.primary,
+ )
+ }
+}
diff --git a/android/app/src/main/res/values-ru/strings.xml b/android/app/src/main/res/values-ru/strings.xml
index cedccf8..dd5371e 100644
--- a/android/app/src/main/res/values-ru/strings.xml
+++ b/android/app/src/main/res/values-ru/strings.xml
@@ -341,6 +341,11 @@
Контакты
Поиск контактов/пользователей
Добавить по email
+ Быстрые действия
+ Найти людей
+ Поиск по имени или юзернейму.
+ Добавить по email
+ Пригласить или добавить контакт напрямую.
Результаты поиска
Мои контакты
был(а) недавно
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index b4d5030..973a9f4 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -341,6 +341,11 @@
Contacts
Search contacts/users
Add by email
+ Quick actions
+ Find people
+ Search by name or username.
+ Add by email
+ Invite or add a contact directly.
Search results
My contacts
last seen recently