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("""
|
@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>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Получить диалог
|
* Получить диалог
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user