feat: redesign contacts screen with card layout
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user