Sync phone contacts and enable chat deletion
This commit is contained in:
@@ -22,8 +22,8 @@ android {
|
||||
applicationId = "xyz.kusoft.qmax"
|
||||
minSdk = 26
|
||||
targetSdk = 36
|
||||
versionCode = 52
|
||||
versionName = "0.1.51"
|
||||
versionCode = 54
|
||||
versionName = "0.1.53"
|
||||
|
||||
buildConfigField("String", "QMAX_DEFAULT_SERVER_URL", "\"https://qmax.kusoft.xyz\"")
|
||||
buildConfigField("String", "QMAX_DEFAULT_PAIRING_CODE", "\"qmax-MxRq4h2HQBEIFs6k\"")
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
|
||||
@@ -2,6 +2,7 @@ package xyz.kusoft.qmax
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.ContentResolver
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.BitmapFactory
|
||||
import android.media.MediaRecorder
|
||||
@@ -9,6 +10,7 @@ import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.provider.ContactsContract
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.BackHandler
|
||||
@@ -78,6 +80,7 @@ import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.NotificationsOff
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.PersonAdd
|
||||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.PushPin
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
@@ -169,6 +172,7 @@ import xyz.kusoft.qmax.core.model.ContactDto
|
||||
import xyz.kusoft.qmax.core.model.MaxBridgeStatusDto
|
||||
import xyz.kusoft.qmax.core.model.MaxChannelSearchResultDto
|
||||
import xyz.kusoft.qmax.core.model.MessageDto
|
||||
import xyz.kusoft.qmax.core.model.PhoneContactDto
|
||||
import xyz.kusoft.qmax.core.model.QMaxSession
|
||||
import xyz.kusoft.qmax.ui.QMaxUiState
|
||||
import xyz.kusoft.qmax.ui.QMaxViewModel
|
||||
@@ -217,6 +221,39 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private fun Intent?.chatIdExtra(): String? = this?.getStringExtra("chatId")?.takeIf { it.isNotBlank() }
|
||||
|
||||
private fun readPhoneContacts(contentResolver: ContentResolver): List<PhoneContactDto> {
|
||||
val projection = arrayOf(
|
||||
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
|
||||
ContactsContract.CommonDataKinds.Phone.NUMBER,
|
||||
ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER
|
||||
)
|
||||
val seenNumbers = mutableSetOf<String>()
|
||||
val result = mutableListOf<PhoneContactDto>()
|
||||
contentResolver.query(
|
||||
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
|
||||
projection,
|
||||
null,
|
||||
null,
|
||||
"${ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME} COLLATE LOCALIZED ASC"
|
||||
)?.use { cursor ->
|
||||
val nameIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME)
|
||||
val numberIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
|
||||
val normalizedIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER)
|
||||
while (cursor.moveToNext()) {
|
||||
val rawNumber = (cursor.getString(normalizedIndex)?.takeIf { it.isNotBlank() }
|
||||
?: cursor.getString(numberIndex)).orEmpty()
|
||||
val normalizedNumber = rawNumber.filter { it.isDigit() || it == '+' }
|
||||
if (normalizedNumber.isBlank() || !seenNumbers.add(normalizedNumber)) continue
|
||||
val displayName = cursor.getString(nameIndex)?.trim().orEmpty().ifBlank { normalizedNumber }
|
||||
result += PhoneContactDto(
|
||||
phoneNumber = normalizedNumber,
|
||||
firstName = displayName
|
||||
)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QMaxApp(vm: QMaxViewModel) {
|
||||
val state by vm.state
|
||||
@@ -344,7 +381,8 @@ private fun LoginScreen(vm: QMaxViewModel) {
|
||||
}
|
||||
|
||||
private enum class ChatBulkUiAction {
|
||||
ClearHistory
|
||||
ClearHistory,
|
||||
DeleteChats
|
||||
}
|
||||
|
||||
private enum class MainTab {
|
||||
@@ -362,6 +400,7 @@ private fun ChatListScreen(vm: QMaxViewModel) {
|
||||
var menuOpen by remember { mutableStateOf(false) }
|
||||
var selectionMenuOpen by remember { mutableStateOf(false) }
|
||||
var pendingBulkAction by remember { mutableStateOf<ChatBulkUiAction?>(null) }
|
||||
var pendingContactRemoval by remember { mutableStateOf<ContactDto?>(null) }
|
||||
var searchMode by remember { mutableStateOf(false) }
|
||||
var newChatOpen by remember { mutableStateOf(false) }
|
||||
val filteredChats = remember(state.chats, state.searchQuery, activeTab) {
|
||||
@@ -392,6 +431,29 @@ private fun ChatListScreen(vm: QMaxViewModel) {
|
||||
}
|
||||
}
|
||||
}
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
val importPhoneContacts: () -> Unit = {
|
||||
scope.launch {
|
||||
val contacts = withContext(Dispatchers.IO) { readPhoneContacts(context.contentResolver) }
|
||||
vm.importPhoneContacts(contacts)
|
||||
}
|
||||
Unit
|
||||
}
|
||||
val contactsPermissionLauncher = rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
|
||||
if (granted) {
|
||||
importPhoneContacts()
|
||||
} else {
|
||||
vm.reportContactsPermissionDenied()
|
||||
}
|
||||
}
|
||||
val requestPhoneContactsImport: () -> Unit = {
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
|
||||
importPhoneContacts()
|
||||
} else {
|
||||
contactsPermissionLauncher.launch(Manifest.permission.READ_CONTACTS)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(activeTab, state.searchQuery) {
|
||||
if (activeTab == MainTab.Channels) {
|
||||
@@ -401,6 +463,12 @@ private fun ChatListScreen(vm: QMaxViewModel) {
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(activeTab, state.session?.userName) {
|
||||
if (activeTab == MainTab.Contacts && state.session != null) {
|
||||
requestPhoneContactsImport()
|
||||
}
|
||||
}
|
||||
|
||||
TelegramChatListContent(
|
||||
state = state,
|
||||
filteredChats = filteredChats,
|
||||
@@ -410,7 +478,7 @@ private fun ChatListScreen(vm: QMaxViewModel) {
|
||||
onMenuOpenChange = { menuOpen = it },
|
||||
onRefresh = {
|
||||
vm.loadChats()
|
||||
vm.loadContacts()
|
||||
if (activeTab == MainTab.Contacts) requestPhoneContactsImport()
|
||||
},
|
||||
onCheckUpdates = vm::checkArgusUpdate,
|
||||
onRefreshMaxStatus = vm::loadMaxStatus,
|
||||
@@ -427,6 +495,9 @@ private fun ChatListScreen(vm: QMaxViewModel) {
|
||||
},
|
||||
onOpenChat = vm::openChat,
|
||||
onOpenContact = vm::openContact,
|
||||
onImportPhoneContacts = requestPhoneContactsImport,
|
||||
onAddContact = vm::addContact,
|
||||
onRemoveContact = { pendingContactRemoval = it },
|
||||
onNewChat = {
|
||||
activeTab = MainTab.Contacts
|
||||
vm.updateSearchQuery("")
|
||||
@@ -440,6 +511,10 @@ private fun ChatListScreen(vm: QMaxViewModel) {
|
||||
onClearSelectedHistory = {
|
||||
selectionMenuOpen = false
|
||||
pendingBulkAction = ChatBulkUiAction.ClearHistory
|
||||
},
|
||||
onDeleteSelectedChats = {
|
||||
selectionMenuOpen = false
|
||||
pendingBulkAction = ChatBulkUiAction.DeleteChats
|
||||
}
|
||||
)
|
||||
|
||||
@@ -451,6 +526,7 @@ private fun ChatListScreen(vm: QMaxViewModel) {
|
||||
Text(
|
||||
when (action) {
|
||||
ChatBulkUiAction.ClearHistory -> "Очистить историю"
|
||||
ChatBulkUiAction.DeleteChats -> "Удалить чаты"
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -458,6 +534,7 @@ private fun ChatListScreen(vm: QMaxViewModel) {
|
||||
Text(
|
||||
when (action) {
|
||||
ChatBulkUiAction.ClearHistory -> "Очистить историю в выбранных чатах: $selectedCount?"
|
||||
ChatBulkUiAction.DeleteChats -> "Удалить выбранные чаты из MAX: $selectedCount? Это действие нельзя отменить."
|
||||
}
|
||||
)
|
||||
},
|
||||
@@ -467,12 +544,14 @@ private fun ChatListScreen(vm: QMaxViewModel) {
|
||||
pendingBulkAction = null
|
||||
when (action) {
|
||||
ChatBulkUiAction.ClearHistory -> vm.clearSelectedChatHistories()
|
||||
ChatBulkUiAction.DeleteChats -> vm.deleteSelectedChats()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
when (action) {
|
||||
ChatBulkUiAction.ClearHistory -> "Очистить"
|
||||
ChatBulkUiAction.ClearHistory -> "Очистить"
|
||||
ChatBulkUiAction.DeleteChats -> "Удалить"
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -485,6 +564,23 @@ private fun ChatListScreen(vm: QMaxViewModel) {
|
||||
)
|
||||
}
|
||||
|
||||
pendingContactRemoval?.let { contact ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { pendingContactRemoval = null },
|
||||
title = { Text("Удалить контакт") },
|
||||
text = { Text("Удалить ${contact.displayName} из контактов MAX? Чат и сообщения останутся.") },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
pendingContactRemoval = null
|
||||
vm.removeContact(contact)
|
||||
}) { Text("Удалить") }
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { pendingContactRemoval = null }) { Text("Отмена") }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (newChatOpen) {
|
||||
NewChatDialog(
|
||||
vm = vm,
|
||||
@@ -659,6 +755,9 @@ private fun TelegramChatListContent(
|
||||
onTabSelected: (MainTab) -> Unit,
|
||||
onOpenChat: (ChatDto) -> Unit,
|
||||
onOpenContact: (ContactDto) -> Unit,
|
||||
onImportPhoneContacts: () -> Unit,
|
||||
onAddContact: (ContactDto) -> Unit,
|
||||
onRemoveContact: (ContactDto) -> Unit,
|
||||
onNewChat: () -> Unit,
|
||||
onCacheAvatar: (String) -> Unit,
|
||||
selectionMenuOpen: Boolean,
|
||||
@@ -666,7 +765,8 @@ private fun TelegramChatListContent(
|
||||
onClearSelection: () -> Unit,
|
||||
onToggleChatSelection: (String) -> Unit,
|
||||
onBeginChatSelection: (String) -> Unit,
|
||||
onClearSelectedHistory: () -> Unit
|
||||
onClearSelectedHistory: () -> Unit,
|
||||
onDeleteSelectedChats: () -> Unit
|
||||
) {
|
||||
val selectedCount = state.selectedChatIds.size
|
||||
val listTab = activeTab != MainTab.Settings
|
||||
@@ -698,7 +798,8 @@ private fun TelegramChatListContent(
|
||||
menuOpen = selectionMenuOpen,
|
||||
onMenuOpenChange = onSelectionMenuOpenChange,
|
||||
onClearSelection = onClearSelection,
|
||||
onClearHistory = onClearSelectedHistory
|
||||
onClearHistory = onClearSelectedHistory,
|
||||
onDeleteChats = onDeleteSelectedChats
|
||||
)
|
||||
} else {
|
||||
TelegramLikeHeader(
|
||||
@@ -762,6 +863,8 @@ private fun TelegramChatListContent(
|
||||
session = state.session,
|
||||
cachedAvatarPaths = state.cachedAvatarPaths,
|
||||
onCacheAvatar = onCacheAvatar,
|
||||
onAdd = { onAddContact(contact) },
|
||||
onRemove = { onRemoveContact(contact) },
|
||||
onClick = { onOpenContact(contact) }
|
||||
)
|
||||
}
|
||||
@@ -798,6 +901,19 @@ private fun TelegramChatListContent(
|
||||
.padding(end = 22.dp, bottom = 96.dp),
|
||||
onNewChat = onNewChat
|
||||
)
|
||||
} else if (activeTab == MainTab.Contacts) {
|
||||
FloatingActionButton(
|
||||
onClick = onImportPhoneContacts,
|
||||
containerColor = QMaxBlue,
|
||||
contentColor = Color.White,
|
||||
shape = CircleShape,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.navigationBarsPadding()
|
||||
.padding(end = 22.dp, bottom = 96.dp)
|
||||
) {
|
||||
Icon(Icons.Filled.Refresh, contentDescription = "Синхронизировать контакты телефона")
|
||||
}
|
||||
}
|
||||
TelegramBottomNavigation(
|
||||
modifier = Modifier
|
||||
@@ -902,7 +1018,8 @@ private fun ChatSelectionHeader(
|
||||
menuOpen: Boolean,
|
||||
onMenuOpenChange: (Boolean) -> Unit,
|
||||
onClearSelection: () -> Unit,
|
||||
onClearHistory: () -> Unit
|
||||
onClearHistory: () -> Unit,
|
||||
onDeleteChats: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
@@ -951,6 +1068,13 @@ private fun ChatSelectionHeader(
|
||||
onClearHistory()
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text("Удалить чат") },
|
||||
onClick = {
|
||||
onMenuOpenChange(false)
|
||||
onDeleteChats()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1004,6 +1128,8 @@ private fun ContactRow(
|
||||
session: QMaxSession?,
|
||||
cachedAvatarPaths: Map<String, String>,
|
||||
onCacheAvatar: (String) -> Unit,
|
||||
onAdd: () -> Unit,
|
||||
onRemove: () -> Unit,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val subtitle = contact.status?.takeIf { it.isNotBlank() }
|
||||
@@ -1043,6 +1169,13 @@ private fun ContactRow(
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
IconButton(onClick = if (contact.isSavedContact) onRemove else onAdd) {
|
||||
Icon(
|
||||
if (contact.isSavedContact) Icons.Filled.Delete else Icons.Filled.PersonAdd,
|
||||
contentDescription = if (contact.isSavedContact) "Удалить из MAX" else "Добавить в MAX",
|
||||
tint = if (contact.isSavedContact) QMaxMuted else QMaxBlue
|
||||
)
|
||||
}
|
||||
Icon(Icons.Filled.Chat, contentDescription = "Открыть чат", tint = QMaxBlue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import xyz.kusoft.qmax.core.model.AuthResponse
|
||||
import xyz.kusoft.qmax.core.model.ChatDto
|
||||
import xyz.kusoft.qmax.core.model.ChatPresenceDto
|
||||
import xyz.kusoft.qmax.core.model.ContactDto
|
||||
import xyz.kusoft.qmax.core.model.PhoneContactDto
|
||||
import xyz.kusoft.qmax.core.model.MaxBridgeStatusDto
|
||||
import xyz.kusoft.qmax.core.model.MaxChannelSearchResultDto
|
||||
import xyz.kusoft.qmax.core.model.MessageDto
|
||||
@@ -96,6 +97,18 @@ class QMaxRepository(
|
||||
return withFreshSession(session) { api.contacts(it.serverUrl, it.accessToken) }
|
||||
}
|
||||
|
||||
suspend fun importContacts(session: QMaxSession, contacts: List<PhoneContactDto>): List<ContactDto> {
|
||||
return withFreshSession(session) { api.importContacts(it.serverUrl, it.accessToken, contacts) }
|
||||
}
|
||||
|
||||
suspend fun addContact(session: QMaxSession, userId: String): ContactDto {
|
||||
return withFreshSession(session) { api.addContact(it.serverUrl, it.accessToken, userId) }
|
||||
}
|
||||
|
||||
suspend fun removeContact(session: QMaxSession, userId: String) {
|
||||
withFreshSession(session) { api.removeContact(it.serverUrl, it.accessToken, userId) }
|
||||
}
|
||||
|
||||
suspend fun createChat(session: QMaxSession, externalId: String, title: String, avatarUrl: String? = null): ChatDto {
|
||||
val chat = withFreshSession(session) {
|
||||
api.createDirect(it.serverUrl, it.accessToken, externalId, title, avatarUrl)
|
||||
@@ -144,6 +157,15 @@ class QMaxRepository(
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteChats(session: QMaxSession, chatIds: Collection<String>) {
|
||||
val ids = chatIds.filter(String::isNotBlank).distinct()
|
||||
if (ids.isEmpty()) return
|
||||
withFreshSession(session) { api.deleteChats(it.serverUrl, it.accessToken, ids) }
|
||||
val remainingChats = cachedChats(session).filterNot { it.id in ids }
|
||||
messageCache.saveChats(session, orderedChats(remainingChats))
|
||||
messageCache.clearMessages(session, ids)
|
||||
}
|
||||
|
||||
suspend fun searchMessages(session: QMaxSession, chatId: String, query: String): List<MessageDto> {
|
||||
val messages = withFreshSession(session) { api.searchMessages(it.serverUrl, it.accessToken, chatId, query) }
|
||||
messages.forEach { messageCache.upsertMessage(session, it) }
|
||||
|
||||
@@ -56,9 +56,23 @@ data class ContactDto(
|
||||
val displayName: String,
|
||||
val avatarUrl: String? = null,
|
||||
val phoneNumber: String? = null,
|
||||
val status: String? = null
|
||||
val status: String? = null,
|
||||
val isSavedContact: Boolean = false
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PhoneContactDto(
|
||||
val phoneNumber: String,
|
||||
val firstName: String,
|
||||
val lastName: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ImportPhoneContactsRequest(val contacts: List<PhoneContactDto>)
|
||||
|
||||
@Serializable
|
||||
data class ContactUserRequest(val userId: String)
|
||||
|
||||
@Serializable
|
||||
data class MessageDto(
|
||||
val id: String,
|
||||
|
||||
@@ -27,13 +27,16 @@ import xyz.kusoft.qmax.core.model.ChatDto
|
||||
import xyz.kusoft.qmax.core.model.ChatPresenceDto
|
||||
import xyz.kusoft.qmax.core.model.CreateDirectChatRequest
|
||||
import xyz.kusoft.qmax.core.model.ContactDto
|
||||
import xyz.kusoft.qmax.core.model.ContactUserRequest
|
||||
import xyz.kusoft.qmax.core.model.DeviceLoginRequest
|
||||
import xyz.kusoft.qmax.core.model.EditMessageRequest
|
||||
import xyz.kusoft.qmax.core.model.ForwardMessageRequest
|
||||
import xyz.kusoft.qmax.core.model.MarkChatReadRequest
|
||||
import xyz.kusoft.qmax.core.model.ImportPhoneContactsRequest
|
||||
import xyz.kusoft.qmax.core.model.MaxBridgeStatusDto
|
||||
import xyz.kusoft.qmax.core.model.MaxChannelSearchResultDto
|
||||
import xyz.kusoft.qmax.core.model.MessageDto
|
||||
import xyz.kusoft.qmax.core.model.PhoneContactDto
|
||||
import xyz.kusoft.qmax.core.model.RegisterPushDeviceRequest
|
||||
import xyz.kusoft.qmax.core.model.SendMessageRequest
|
||||
import xyz.kusoft.qmax.core.model.SetReactionRequest
|
||||
@@ -82,6 +85,18 @@ class QMaxApi {
|
||||
return get(sessionServerUrl, "/api/contacts", token)
|
||||
}
|
||||
|
||||
suspend fun importContacts(sessionServerUrl: String, token: String, contacts: List<PhoneContactDto>): List<ContactDto> {
|
||||
return post(sessionServerUrl, "/api/contacts/import", token, ImportPhoneContactsRequest(contacts))
|
||||
}
|
||||
|
||||
suspend fun addContact(sessionServerUrl: String, token: String, userId: String): ContactDto {
|
||||
return post(sessionServerUrl, "/api/contacts/add", token, ContactUserRequest(userId))
|
||||
}
|
||||
|
||||
suspend fun removeContact(sessionServerUrl: String, token: String, userId: String) {
|
||||
postNoContent(sessionServerUrl, "/api/contacts/remove", token, ContactUserRequest(userId))
|
||||
}
|
||||
|
||||
suspend fun createDirect(
|
||||
sessionServerUrl: String,
|
||||
token: String,
|
||||
@@ -104,6 +119,10 @@ class QMaxApi {
|
||||
postNoContent(sessionServerUrl, "/api/chats/clear-history", token, ChatBulkActionRequest(chatIds))
|
||||
}
|
||||
|
||||
suspend fun deleteChats(sessionServerUrl: String, token: String, chatIds: List<String>) {
|
||||
postNoContent(sessionServerUrl, "/api/chats/delete", token, ChatBulkActionRequest(chatIds))
|
||||
}
|
||||
|
||||
suspend fun searchMessages(sessionServerUrl: String, token: String, chatId: String, query: String): List<MessageDto> {
|
||||
val encodedQuery = URLEncoder.encode(query, StandardCharsets.UTF_8.toString())
|
||||
return get(sessionServerUrl, "/api/chats/$chatId/search?q=$encodedQuery", token)
|
||||
|
||||
@@ -21,6 +21,7 @@ import xyz.kusoft.qmax.core.model.MaxBridgeStatusDto
|
||||
import xyz.kusoft.qmax.core.model.MaxChannelSearchResultDto
|
||||
import xyz.kusoft.qmax.core.model.MessageDeletedDto
|
||||
import xyz.kusoft.qmax.core.model.MessageDto
|
||||
import xyz.kusoft.qmax.core.model.PhoneContactDto
|
||||
import xyz.kusoft.qmax.core.model.QMaxSession
|
||||
import xyz.kusoft.qmax.core.network.QMaxHttpException
|
||||
import xyz.kusoft.qmax.core.realtime.QMaxRealtimeClient
|
||||
@@ -420,6 +421,64 @@ class QMaxViewModel(private val repository: QMaxRepository) : ViewModel() {
|
||||
loadChats()
|
||||
}
|
||||
|
||||
fun reportContactsPermissionDenied() {
|
||||
state.value = state.value.copy(
|
||||
contactsLoading = false,
|
||||
contactsError = "Разрешите QMAX доступ к контактам телефона и обновите список"
|
||||
)
|
||||
}
|
||||
|
||||
fun importPhoneContacts(contacts: List<PhoneContactDto>) = viewModelScope.launch {
|
||||
state.value = state.value.copy(contactsLoading = true, contactsError = null, error = null)
|
||||
if (contacts.isEmpty()) {
|
||||
state.value = state.value.copy(
|
||||
contacts = emptyList(),
|
||||
contactsLoading = false,
|
||||
contactsError = "В телефонной книге нет контактов с номерами"
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
try {
|
||||
val session = requireSession()
|
||||
val imported = repository.importContacts(session, contacts)
|
||||
state.value = state.value.copy(
|
||||
contacts = imported.sortedBy { it.displayName.lowercase() },
|
||||
contactsLoading = false,
|
||||
contactsError = if (imported.isEmpty()) {
|
||||
"MAX не сопоставил ни один из ${contacts.size} номеров телефонной книги"
|
||||
} else {
|
||||
null
|
||||
},
|
||||
updateMessage = "MAX сопоставил контактов: ${imported.size}"
|
||||
)
|
||||
} catch (error: Throwable) {
|
||||
if (error is CancellationException) throw error
|
||||
Log.w(LogTag, "Phone contacts import failed", error)
|
||||
state.value = state.value.copy(
|
||||
contactsLoading = false,
|
||||
contactsError = error.message ?: "Не удалось синхронизировать контакты"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addContact(contact: ContactDto) = launchLoading(false) {
|
||||
val session = requireSession()
|
||||
val updated = repository.addContact(session, contact.userId)
|
||||
state.value = state.value.copy(
|
||||
contacts = state.value.contacts.map { if (it.userId == updated.userId) updated else it }
|
||||
)
|
||||
}
|
||||
|
||||
fun removeContact(contact: ContactDto) = launchLoading(false) {
|
||||
val session = requireSession()
|
||||
repository.removeContact(session, contact.userId)
|
||||
state.value = state.value.copy(
|
||||
contacts = state.value.contacts.map {
|
||||
if (it.userId == contact.userId) it.copy(isSavedContact = false) else it
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun openChat(chat: ChatDto) {
|
||||
saveCurrentDraft()
|
||||
chatSearchJob?.cancel()
|
||||
@@ -550,6 +609,17 @@ class QMaxViewModel(private val repository: QMaxRepository) : ViewModel() {
|
||||
refreshChats(showLoading = false)
|
||||
}
|
||||
|
||||
fun deleteSelectedChats() = launchLoading(false) {
|
||||
val session = requireSession()
|
||||
val ids = state.value.selectedChatIds
|
||||
if (ids.isEmpty()) return@launchLoading
|
||||
repository.deleteChats(session, ids)
|
||||
state.value = state.value.copy(
|
||||
chats = state.value.chats.filterNot { it.id in ids },
|
||||
selectedChatIds = emptySet()
|
||||
)
|
||||
}
|
||||
|
||||
fun loadMessages(showLoading: Boolean = true, forceHydrate: Boolean = false) = launchLoading(showLoading) {
|
||||
if (!showLoading && state.value.sendingMessage) {
|
||||
return@launchLoading
|
||||
|
||||
Reference in New Issue
Block a user