feat: Add requests handling in ChatsListViewModel and UI; implement RequestsScreen and RequestsSection for better user interaction

This commit is contained in:
k1ngsterr1
2026-01-16 18:08:34 +05:00
parent 6d506e681b
commit 7750f450e8
3 changed files with 253 additions and 5 deletions

View File

@@ -290,14 +290,52 @@ interface DialogDao {
/** /**
* Получить все диалоги отсортированные по последнему сообщению * Получить все диалоги отсортированные по последнему сообщению
* Исключает requests (диалоги без исходящих сообщений от нас)
*/ */
@Query(""" @Query("""
SELECT * FROM dialogs SELECT d.* FROM dialogs d
WHERE account = :account WHERE d.account = :account
ORDER BY last_message_timestamp DESC AND EXISTS (
SELECT 1 FROM messages m
WHERE m.account = :account
AND m.from_public_key = :account
AND m.to_public_key = d.opponent_key
)
ORDER BY d.last_message_timestamp DESC
""") """)
fun getDialogsFlow(account: String): Flow<List<DialogEntity>> fun getDialogsFlow(account: String): Flow<List<DialogEntity>>
/**
* Получить requests - диалоги где нам писали, но мы не отвечали
*/
@Query("""
SELECT d.* FROM dialogs d
WHERE d.account = :account
AND NOT EXISTS (
SELECT 1 FROM messages m
WHERE m.account = :account
AND m.from_public_key = :account
AND m.to_public_key = d.opponent_key
)
ORDER BY d.last_message_timestamp DESC
""")
fun getRequestsFlow(account: String): Flow<List<DialogEntity>>
/**
* Получить количество requests
*/
@Query("""
SELECT COUNT(*) FROM dialogs d
WHERE d.account = :account
AND NOT EXISTS (
SELECT 1 FROM messages m
WHERE m.account = :account
AND m.from_public_key = :account
AND m.to_public_key = d.opponent_key
)
""")
fun getRequestsCountFlow(account: String): Flow<Int>
/** /**
* Получить диалог * Получить диалог
*/ */

View File

@@ -508,9 +508,28 @@ fun ChatsListScreen(
}, },
containerColor = backgroundColor containerColor = backgroundColor
) { paddingValues -> ) { paddingValues ->
// 📬 State for showing requests screen
var showRequestsScreen by remember { mutableStateOf(false) }
// Main content // Main content
Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) { Box(modifier = Modifier.fillMaxSize().padding(paddingValues)) {
if (dialogsList.isEmpty()) { // 📬 Requests count from ViewModel
val requestsCount by chatsViewModel.requestsCount.collectAsState()
val requests by chatsViewModel.requests.collectAsState()
if (showRequestsScreen) {
// 📬 Show Requests Screen
RequestsScreen(
requests = requests,
isDarkTheme = isDarkTheme,
onBack = { showRequestsScreen = false },
onRequestClick = { request ->
showRequestsScreen = false
val user = chatsViewModel.dialogToSearchUser(request)
onUserSelect(user)
}
)
} else if (dialogsList.isEmpty() && requestsCount == 0) {
// Empty state with Lottie animation // Empty state with Lottie animation
EmptyChatsState( EmptyChatsState(
isDarkTheme = isDarkTheme, isDarkTheme = isDarkTheme,
@@ -521,6 +540,21 @@ fun ChatsListScreen(
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8) val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
LazyColumn(modifier = Modifier.fillMaxSize()) { LazyColumn(modifier = Modifier.fillMaxSize()) {
// 📬 Requests Section
if (requestsCount > 0) {
item(key = "requests_section") {
RequestsSection(
count = requestsCount,
isDarkTheme = isDarkTheme,
onClick = { showRequestsScreen = true }
)
Divider(
color = dividerColor,
thickness = 0.5.dp
)
}
}
items(dialogsList, key = { it.opponentKey }) { dialog -> items(dialogsList, key = { it.opponentKey }) { dialog ->
val isSavedMessages = dialog.opponentKey == accountPublicKey val isSavedMessages = dialog.opponentKey == accountPublicKey
// Check if user is blocked // Check if user is blocked
@@ -1304,3 +1338,124 @@ fun TypingIndicatorSmall() {
} }
} }
} }
/**
* 📬 Секция Requests - кнопка для перехода к списку запросов
*/
@Composable
fun RequestsSection(
count: Int,
isDarkTheme: Boolean,
onClick: () -> Unit
) {
val textColor = if (isDarkTheme) Color(0xFF4DABF7) else Color(0xFF228BE6)
val arrowColor = if (isDarkTheme) Color(0xFFC9C9C9) else Color(0xFF228BE6)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 14.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Requests +$count",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
color = textColor
)
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = "Open requests",
tint = arrowColor,
modifier = Modifier.size(24.dp)
)
}
}
/**
* 📬 Экран со списком Requests
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RequestsScreen(
requests: List<DialogUiModel>,
isDarkTheme: Boolean,
onBack: () -> Unit,
onRequestClick: (DialogUiModel) -> Unit
) {
val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8)
Column(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
) {
// Header
TopAppBar(
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Back",
tint = PrimaryBlue
)
}
},
title = {
Text(
text = "Requests",
fontWeight = FontWeight.Bold,
fontSize = 20.sp,
color = textColor
)
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = backgroundColor
)
)
Divider(color = dividerColor, thickness = 0.5.dp)
if (requests.isEmpty()) {
// Empty state
Box(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "No requests",
fontSize = 16.sp,
color = if (isDarkTheme) Color(0xFF8E8E8E) else Color(0xFF8E8E93),
textAlign = TextAlign.Center
)
}
} else {
// Requests list
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
items(requests, key = { it.opponentKey }) { request ->
DialogItem(
dialog = request,
isDarkTheme = isDarkTheme,
isTyping = false,
onClick = { onRequestClick(request) }
)
Divider(
modifier = Modifier.padding(start = 84.dp),
color = dividerColor,
thickness = 0.5.dp
)
}
}
}
}
}

View File

@@ -46,6 +46,14 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
private val _dialogs = MutableStateFlow<List<DialogUiModel>>(emptyList()) private val _dialogs = MutableStateFlow<List<DialogUiModel>>(emptyList())
val dialogs: StateFlow<List<DialogUiModel>> = _dialogs.asStateFlow() val dialogs: StateFlow<List<DialogUiModel>> = _dialogs.asStateFlow()
// Список requests (запросы от новых пользователей)
private val _requests = MutableStateFlow<List<DialogUiModel>>(emptyList())
val requests: StateFlow<List<DialogUiModel>> = _requests.asStateFlow()
// Количество requests
private val _requestsCount = MutableStateFlow(0)
val requestsCount: StateFlow<Int> = _requestsCount.asStateFlow()
// Загрузка // Загрузка
private val _isLoading = MutableStateFlow(false) private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow() val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
@@ -60,7 +68,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
currentAccount = publicKey currentAccount = publicKey
currentPrivateKey = privateKey currentPrivateKey = privateKey
// Подписываемся на обычные диалоги
viewModelScope.launch { viewModelScope.launch {
dialogDao.getDialogsFlow(publicKey) dialogDao.getDialogsFlow(publicKey)
.flowOn(Dispatchers.IO) // 🚀 Flow работает на IO .flowOn(Dispatchers.IO) // 🚀 Flow работает на IO
@@ -102,6 +110,53 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
subscribeToOnlineStatuses(decryptedDialogs.map { it.opponentKey }, privateKey) subscribeToOnlineStatuses(decryptedDialogs.map { it.opponentKey }, privateKey)
} }
} }
// 📬 Подписываемся на requests (запросы от новых пользователей)
viewModelScope.launch {
dialogDao.getRequestsFlow(publicKey)
.flowOn(Dispatchers.IO)
.map { requestsList ->
requestsList.map { dialog ->
val decryptedLastMessage = try {
if (privateKey.isNotEmpty() && dialog.lastMessage.isNotEmpty()) {
CryptoManager.decryptWithPassword(dialog.lastMessage, privateKey)
?: dialog.lastMessage
} else {
dialog.lastMessage
}
} catch (e: Exception) {
dialog.lastMessage
}
DialogUiModel(
id = dialog.id,
account = dialog.account,
opponentKey = dialog.opponentKey,
opponentTitle = dialog.opponentTitle,
opponentUsername = dialog.opponentUsername,
lastMessage = decryptedLastMessage,
lastMessageTimestamp = dialog.lastMessageTimestamp,
unreadCount = dialog.unreadCount,
isOnline = dialog.isOnline,
lastSeen = dialog.lastSeen,
verified = dialog.verified
)
}
}
.flowOn(Dispatchers.Default)
.collect { decryptedRequests ->
_requests.value = decryptedRequests
}
}
// 📊 Подписываемся на количество requests
viewModelScope.launch {
dialogDao.getRequestsCountFlow(publicKey)
.flowOn(Dispatchers.IO)
.collect { count ->
_requestsCount.value = count
}
}
} }
/** /**