From 7750f450e821e0205944eb2b65f59d474bf546be Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Fri, 16 Jan 2026 18:08:34 +0500 Subject: [PATCH] feat: Add requests handling in ChatsListViewModel and UI; implement RequestsScreen and RequestsSection for better user interaction --- .../messenger/database/MessageEntities.kt | 44 ++++- .../messenger/ui/chats/ChatsListScreen.kt | 157 +++++++++++++++++- .../messenger/ui/chats/ChatsListViewModel.kt | 57 ++++++- 3 files changed, 253 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt index 69a74fe..fc833e2 100644 --- a/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt +++ b/app/src/main/java/com/rosetta/messenger/database/MessageEntities.kt @@ -290,14 +290,52 @@ interface DialogDao { /** * Получить все диалоги отсортированные по последнему сообщению + * Исключает requests (диалоги без исходящих сообщений от нас) */ @Query(""" - SELECT * FROM dialogs - WHERE account = :account - ORDER BY last_message_timestamp DESC + SELECT d.* FROM dialogs d + WHERE d.account = :account + 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> + /** + * Получить 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> + + /** + * Получить количество 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 + /** * Получить диалог */ diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index bdd134b..98d83ae 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -508,9 +508,28 @@ fun ChatsListScreen( }, containerColor = backgroundColor ) { paddingValues -> + // 📬 State for showing requests screen + var showRequestsScreen by remember { mutableStateOf(false) } + // Main content 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 EmptyChatsState( isDarkTheme = isDarkTheme, @@ -521,6 +540,21 @@ fun ChatsListScreen( val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8) 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 -> val isSavedMessages = dialog.opponentKey == accountPublicKey // 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, + 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 + ) + } + } + } + } +} diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt index 73058a0..b656e0d 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt @@ -46,6 +46,14 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio private val _dialogs = MutableStateFlow>(emptyList()) val dialogs: StateFlow> = _dialogs.asStateFlow() + // Список requests (запросы от новых пользователей) + private val _requests = MutableStateFlow>(emptyList()) + val requests: StateFlow> = _requests.asStateFlow() + + // Количество requests + private val _requestsCount = MutableStateFlow(0) + val requestsCount: StateFlow = _requestsCount.asStateFlow() + // Загрузка private val _isLoading = MutableStateFlow(false) val isLoading: StateFlow = _isLoading.asStateFlow() @@ -60,7 +68,7 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio currentAccount = publicKey currentPrivateKey = privateKey - + // Подписываемся на обычные диалоги viewModelScope.launch { dialogDao.getDialogsFlow(publicKey) .flowOn(Dispatchers.IO) // 🚀 Flow работает на IO @@ -102,6 +110,53 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio 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 + } + } } /**