feat: redesign contacts screen with card layout
This commit is contained in:
@@ -1,34 +1,48 @@
|
|||||||
package ru.daemonlord.messenger.ui.contacts
|
package ru.daemonlord.messenger.ui.contacts
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
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.widthIn
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
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.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -119,27 +133,83 @@ private fun ContactsScreen(
|
|||||||
.padding(horizontal = 16.dp),
|
.padding(horizontal = 16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
Surface(
|
||||||
value = state.query,
|
|
||||||
onValueChange = onQueryChanged,
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
shape = CircleShape,
|
||||||
label = { Text(stringResource(id = R.string.contacts_search_label)) },
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.42f),
|
||||||
)
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = state.addByEmail,
|
value = state.query,
|
||||||
onValueChange = onAddByEmailChanged,
|
onValueChange = onQueryChanged,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
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) {
|
if (state.isSearchingUsers || state.query.trim().length >= 2) {
|
||||||
item(key = "search_header") {
|
item(key = "search_header") {
|
||||||
Text(
|
ContactsSectionHeader(title = stringResource(id = R.string.contacts_search_results))
|
||||||
text = stringResource(id = R.string.contacts_search_results),
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
items(state.searchResults, key = { "search_${it.id}" }) { user ->
|
items(state.searchResults, key = { "search_${it.id}" }) { user ->
|
||||||
Row(
|
ContactRowCard(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
name = user.name,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
subtitle = user.username?.let { "@$it" } ?: "id: ${user.id}",
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
actionLabel = stringResource(id = R.string.common_create),
|
||||||
) {
|
onAction = { onAddContact(user.id) },
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item(key = "contacts_header") {
|
item(key = "contacts_header") {
|
||||||
Text(
|
ContactsSectionHeader(
|
||||||
text = stringResource(id = R.string.contacts_my_contacts),
|
title = stringResource(id = R.string.contacts_my_contacts),
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
modifier = Modifier.padding(top = 4.dp),
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
items(state.contacts, key = { "contact_${it.id}" }) { contact ->
|
items(state.contacts, key = { "contact_${it.id}" }) { contact ->
|
||||||
Row(
|
ContactRowCard(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
name = contact.name,
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
subtitle = contact.username?.let { "@$it" } ?: stringResource(id = R.string.contacts_last_seen_recently),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
actionLabel = stringResource(id = R.string.contacts_remove),
|
||||||
) {
|
onAction = { onRemoveContact(contact.id) },
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.contacts.isEmpty()) {
|
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_title">Контакты</string>
|
||||||
<string name="contacts_search_label">Поиск контактов/пользователей</string>
|
<string name="contacts_search_label">Поиск контактов/пользователей</string>
|
||||||
<string name="contacts_add_by_email_label">Добавить по email</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_search_results">Результаты поиска</string>
|
||||||
<string name="contacts_my_contacts">Мои контакты</string>
|
<string name="contacts_my_contacts">Мои контакты</string>
|
||||||
<string name="contacts_last_seen_recently">был(а) недавно</string>
|
<string name="contacts_last_seen_recently">был(а) недавно</string>
|
||||||
|
|||||||
@@ -341,6 +341,11 @@
|
|||||||
<string name="contacts_title">Contacts</string>
|
<string name="contacts_title">Contacts</string>
|
||||||
<string name="contacts_search_label">Search contacts/users</string>
|
<string name="contacts_search_label">Search contacts/users</string>
|
||||||
<string name="contacts_add_by_email_label">Add by email</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_search_results">Search results</string>
|
||||||
<string name="contacts_my_contacts">My contacts</string>
|
<string name="contacts_my_contacts">My contacts</string>
|
||||||
<string name="contacts_last_seen_recently">last seen recently</string>
|
<string name="contacts_last_seen_recently">last seen recently</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user