feat: Add requests handling in ChatsListViewModel and UI; implement RequestsScreen and RequestsSection for better user interaction
This commit is contained in:
@@ -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<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>
|
||||
|
||||
/**
|
||||
* Получить диалог
|
||||
*/
|
||||
|
||||
@@ -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<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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,14 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio
|
||||
private val _dialogs = MutableStateFlow<List<DialogUiModel>>(emptyList())
|
||||
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)
|
||||
val isLoading: StateFlow<Boolean> = _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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user