feat: redesign contacts screen with card layout
Some checks failed
Android CI / android (push) Has started running
Android Release / release (push) Has been cancelled
CI / test (push) Has been cancelled

This commit is contained in:
2026-04-06 02:04:15 +03:00
parent e966e54e3a
commit e4dd8df474
3 changed files with 241 additions and 69 deletions

View File

@@ -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,
)
}
}

View File

@@ -341,6 +341,11 @@
<string name="contacts_title">Контакты</string>
<string name="contacts_search_label">Поиск контактов/пользователей</string>
<string name="contacts_add_by_email_label">Добавить по email</string>
<string name="contacts_quick_actions">Быстрые действия</string>
<string name="contacts_action_find_people">Найти людей</string>
<string name="contacts_action_find_people_subtitle">Поиск по имени или юзернейму.</string>
<string name="contacts_action_add_email">Добавить по email</string>
<string name="contacts_action_add_email_subtitle">Пригласить или добавить контакт напрямую.</string>
<string name="contacts_search_results">Результаты поиска</string>
<string name="contacts_my_contacts">Мои контакты</string>
<string name="contacts_last_seen_recently">был(а) недавно</string>

View File

@@ -341,6 +341,11 @@
<string name="contacts_title">Contacts</string>
<string name="contacts_search_label">Search contacts/users</string>
<string name="contacts_add_by_email_label">Add by email</string>
<string name="contacts_quick_actions">Quick actions</string>
<string name="contacts_action_find_people">Find people</string>
<string name="contacts_action_find_people_subtitle">Search by name or username.</string>
<string name="contacts_action_add_email">Add by email</string>
<string name="contacts_action_add_email_subtitle">Invite or add a contact directly.</string>
<string name="contacts_search_results">Search results</string>
<string name="contacts_my_contacts">My contacts</string>
<string name="contacts_last_seen_recently">last seen recently</string>