Профиль, редактирование без аватара
This commit is contained in:
@@ -6,10 +6,15 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material.icons.filled.PersonAdd
|
||||
import androidx.compose.material.icons.filled.Chat
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Cancel
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import contacts.data.remote.api.ContactDto
|
||||
@@ -20,10 +25,15 @@ import ru.knot.messager.R
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ContactListScreen(
|
||||
contacts: List<ContactDto>,
|
||||
contacts: List<contacts.data.remote.api.ContactDto>,
|
||||
requests: List<contacts.data.remote.api.FriendRequestDto> = emptyList(),
|
||||
isLoading: Boolean = false,
|
||||
onContactClick: (String) -> Unit,
|
||||
onSearchChange: (String) -> Unit
|
||||
onSearchChange: (String) -> Unit,
|
||||
onAddContact: (String) -> Unit,
|
||||
onStartChat: (String) -> Unit,
|
||||
onAcceptRequest: (String) -> Unit,
|
||||
onDeclineRequest: (String) -> Unit
|
||||
) {
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
var selectedTab by remember { mutableIntStateOf(0) }
|
||||
@@ -36,9 +46,10 @@ fun ContactListScreen(
|
||||
topBar = {
|
||||
Column {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.contacts_title)) },
|
||||
title = { Text(stringResource(R.string.contacts_title), fontWeight = androidx.compose.ui.text.font.FontWeight.Bold) },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.primaryContainer
|
||||
containerColor = Color.Transparent,
|
||||
titleContentColor = Color.White
|
||||
)
|
||||
)
|
||||
TabRow(selectedTabIndex = selectedTab) {
|
||||
@@ -78,13 +89,57 @@ fun ContactListScreen(
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
} else {
|
||||
val filteredContacts = contacts.filter {
|
||||
if (selectedTab == 1) it.isOnline else true
|
||||
val filteredContacts = if (searchQuery.isNotEmpty()) {
|
||||
contacts
|
||||
} else {
|
||||
contacts.filter {
|
||||
if (selectedTab == 1) it.isOnline else true
|
||||
}
|
||||
}
|
||||
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(filteredContacts) { contact ->
|
||||
ContactItem(contact = contact, onClick = { onContactClick(contact.id) })
|
||||
// Contact Requests Section
|
||||
if (searchQuery.isEmpty() && requests.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "Заявки в контакты",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
items(requests) { request ->
|
||||
ContactRequestItem(
|
||||
request = request,
|
||||
onAccept = { onAcceptRequest(request.id) },
|
||||
onDecline = { onDeclineRequest(request.id) }
|
||||
)
|
||||
}
|
||||
item {
|
||||
Divider(modifier = Modifier.padding(vertical = 8.dp), color = MaterialTheme.colorScheme.outlineVariant)
|
||||
}
|
||||
}
|
||||
|
||||
if (filteredContacts.isEmpty() && requests.isEmpty()) {
|
||||
item {
|
||||
Box(modifier = Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(
|
||||
text = if (searchQuery.isNotEmpty()) "Пользователи не найдены" else "Список контактов пуст",
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
items(filteredContacts) { contact ->
|
||||
ContactItem(
|
||||
contact = contact,
|
||||
onClick = { onContactClick(contact.id) },
|
||||
onAddClick = { onAddContact(contact.id) },
|
||||
onChatClick = { onStartChat(contact.id) },
|
||||
isSearchMode = searchQuery.isNotEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -93,7 +148,56 @@ fun ContactListScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactItem(contact: ContactDto, onClick: () -> Unit) {
|
||||
fun ContactRequestItem(
|
||||
request: contacts.data.remote.api.FriendRequestDto,
|
||||
onAccept: () -> Unit,
|
||||
onDecline: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp, 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AppAvatar(
|
||||
url = request.user.effectiveAvatarUrl,
|
||||
name = request.user.effectiveUsername,
|
||||
size = 48.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = request.user.displayName ?: request.user.effectiveUsername,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
Text(
|
||||
text = if (request.isOutgoing) "Исходящий запрос" else "Входящий запрос",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Row {
|
||||
if (!request.isOutgoing) {
|
||||
IconButton(onClick = onAccept) {
|
||||
Icon(Icons.Default.CheckCircle, contentDescription = "Accept", tint = Color(0xFF4CAF50))
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onDecline) {
|
||||
Icon(Icons.Default.Cancel, contentDescription = "Decline", tint = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContactItem(
|
||||
contact: contacts.data.remote.api.ContactDto,
|
||||
onClick: () -> Unit,
|
||||
onAddClick: () -> Unit,
|
||||
onChatClick: () -> Unit,
|
||||
isSearchMode: Boolean
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@@ -102,8 +206,8 @@ fun ContactItem(contact: ContactDto, onClick: () -> Unit) {
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AppAvatar(
|
||||
url = contact.avatarUrl,
|
||||
name = contact.username,
|
||||
url = contact.effectiveAvatarUrl,
|
||||
name = contact.effectiveUsername,
|
||||
size = 56.dp
|
||||
)
|
||||
|
||||
@@ -111,7 +215,7 @@ fun ContactItem(contact: ContactDto, onClick: () -> Unit) {
|
||||
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = contact.displayName ?: contact.username,
|
||||
text = contact.displayName ?: contact.effectiveUsername,
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
@@ -126,5 +230,24 @@ fun ContactItem(contact: ContactDto, onClick: () -> Unit) {
|
||||
color = if (contact.isOnline) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
Row {
|
||||
if (isSearchMode) {
|
||||
IconButton(onClick = onAddClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PersonAdd,
|
||||
contentDescription = "Add Contact",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
IconButton(onClick = onChatClick) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Chat,
|
||||
contentDescription = "Start Chat",
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,25 +2,121 @@ package contacts.presentation
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import contacts.data.remote.api.ContactDto
|
||||
import chats.domain.repository.ChatRepository
|
||||
import contacts.domain.repository.ContactRepository
|
||||
import core.utils.NavigationManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
data class ContactListState(
|
||||
val contacts: List<ContactDto> = emptyList(),
|
||||
val contacts: List<contacts.data.remote.api.ContactDto> = emptyList(),
|
||||
val requests: List<contacts.data.remote.api.FriendRequestDto> = emptyList(),
|
||||
val isLoading: Boolean = false,
|
||||
val error: String? = null
|
||||
)
|
||||
|
||||
@HiltViewModel
|
||||
class ContactListViewModel @Inject constructor() : ViewModel() {
|
||||
class ContactListViewModel @Inject constructor(
|
||||
private val contactRepository: ContactRepository,
|
||||
private val chatRepository: ChatRepository,
|
||||
private val navigationManager: NavigationManager
|
||||
) : ViewModel() {
|
||||
private val _state = MutableStateFlow(ContactListState())
|
||||
val state: StateFlow<ContactListState> = _state.asStateFlow()
|
||||
|
||||
init {
|
||||
loadData()
|
||||
}
|
||||
|
||||
fun loadData() {
|
||||
loadContacts()
|
||||
loadRequests()
|
||||
}
|
||||
|
||||
fun loadContacts() {
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
contactRepository.getContacts()
|
||||
.onSuccess { contacts ->
|
||||
_state.update { it.copy(contacts = contacts, isLoading = false) }
|
||||
}
|
||||
.onFailure { e ->
|
||||
_state.update { it.copy(isLoading = false, error = e.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadRequests() {
|
||||
viewModelScope.launch {
|
||||
contactRepository.getFriendRequests()
|
||||
.onSuccess { requests ->
|
||||
_state.update { it.copy(requests = requests) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onSearchChange(query: String) {
|
||||
// Заглушка поиска
|
||||
if (query.length < 3) {
|
||||
if (query.isEmpty()) loadData()
|
||||
return
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
_state.update { it.copy(isLoading = true, error = null) }
|
||||
contactRepository.searchUsers(query)
|
||||
.onSuccess { users ->
|
||||
_state.update { it.copy(contacts = users, isLoading = false) }
|
||||
}
|
||||
.onFailure { e ->
|
||||
_state.update { it.copy(isLoading = false, error = e.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addContact(userId: String) {
|
||||
viewModelScope.launch {
|
||||
contactRepository.addContact(userId)
|
||||
.onSuccess {
|
||||
loadData()
|
||||
}
|
||||
.onFailure { e ->
|
||||
_state.update { it.copy(error = e.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun acceptRequest(requestId: String) {
|
||||
viewModelScope.launch {
|
||||
contactRepository.acceptRequest(requestId)
|
||||
.onSuccess { loadData() }
|
||||
.onFailure { e -> _state.update { it.copy(error = e.message) } }
|
||||
}
|
||||
}
|
||||
|
||||
fun declineRequest(requestId: String) {
|
||||
viewModelScope.launch {
|
||||
contactRepository.declineRequest(requestId)
|
||||
.onSuccess { loadData() }
|
||||
.onFailure { e -> _state.update { it.copy(error = e.message) } }
|
||||
}
|
||||
}
|
||||
|
||||
fun startChat(userId: String) {
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
// В вебе мы ищем существующий чат или создаем новый.
|
||||
// В мобилке мы для начала можем просто вызвать createPersonalChat.
|
||||
// Бэкенд обычно возвращает существующий чат, если он уже есть.
|
||||
val chat = chatRepository.createPersonalChat(userId)
|
||||
navigationManager.navigateToChat(chat.id)
|
||||
} catch (e: Exception) {
|
||||
_state.update { it.copy(error = e.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user