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