From 7824ab1a55404ffaae03a92003c6a7ff64692f89 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 9 Mar 2026 22:40:52 +0300 Subject: [PATCH] android: add chat title/profile patch API parity --- android/CHANGELOG.md | 9 ++++ .../messenger/data/chat/api/ChatApiService.kt | 14 +++++++ .../messenger/data/chat/dto/ChatDtos.kt | 13 ++++++ .../chat/repository/NetworkChatRepository.kt | 41 +++++++++++++++++++ .../domain/chat/repository/ChatRepository.kt | 7 ++++ .../messenger/ui/chats/ChatListScreen.kt | 32 +++++++++++++++ .../messenger/ui/chats/ChatListViewModel.kt | 36 ++++++++++++++++ docs/backend-web-android-parity.md | 7 +--- 8 files changed, 154 insertions(+), 5 deletions(-) diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 12d2abd..97dc2b8 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -647,3 +647,12 @@ - re-encode as `image/jpeg` with quality `82`, - keep original bytes if compression does not reduce payload size. - Upload request and attachment metadata now use actual prepared payload (`fileName`, `fileType`, `fileSize`), matching web behavior. + +### Step 101 - Chat title/profile API parity +- Added Android API integration for: + - `PATCH /api/v1/chats/{chat_id}/title` + - `PATCH /api/v1/chats/{chat_id}/profile` +- Extended `ChatRepository`/`NetworkChatRepository` with `updateChatTitle(...)` and `updateChatProfile(...)`. +- Wired these actions into the existing Chat Management panel: + - edit selected chat title, + - edit selected chat profile fields (title/description). diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/chat/api/ChatApiService.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/chat/api/ChatApiService.kt index 7471a3d..cfe286d 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/chat/api/ChatApiService.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/chat/api/ChatApiService.kt @@ -17,7 +17,9 @@ import ru.daemonlord.messenger.data.chat.dto.ChatMemberDto import ru.daemonlord.messenger.data.chat.dto.ChatMemberRoleUpdateRequestDto import ru.daemonlord.messenger.data.chat.dto.ChatNotificationSettingsDto import ru.daemonlord.messenger.data.chat.dto.ChatNotificationSettingsUpdateDto +import ru.daemonlord.messenger.data.chat.dto.ChatProfileUpdateRequestDto import ru.daemonlord.messenger.data.chat.dto.ChatReadDto +import ru.daemonlord.messenger.data.chat.dto.ChatTitleUpdateRequestDto import ru.daemonlord.messenger.data.chat.dto.DiscoverChatDto interface ChatApiService { @@ -95,6 +97,18 @@ interface ChatApiService { @Path("chat_id") chatId: Long, ): ChatReadDto + @PATCH("/api/v1/chats/{chat_id}/title") + suspend fun updateChatTitle( + @Path("chat_id") chatId: Long, + @Body request: ChatTitleUpdateRequestDto, + ): ChatReadDto + + @PATCH("/api/v1/chats/{chat_id}/profile") + suspend fun updateChatProfile( + @Path("chat_id") chatId: Long, + @Body request: ChatProfileUpdateRequestDto, + ): ChatReadDto + @GET("/api/v1/chats/{chat_id}/notifications") suspend fun getChatNotifications( @Path("chat_id") chatId: Long, diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/chat/dto/ChatDtos.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/chat/dto/ChatDtos.kt index 8a898ab..2ad8a1c 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/chat/dto/ChatDtos.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/chat/dto/ChatDtos.kt @@ -132,3 +132,16 @@ data class ChatNotificationSettingsDto( data class ChatNotificationSettingsUpdateDto( val muted: Boolean, ) + +@Serializable +data class ChatTitleUpdateRequestDto( + val title: String, +) + +@Serializable +data class ChatProfileUpdateRequestDto( + val title: String? = null, + val description: String? = null, + @SerialName("avatar_url") + val avatarUrl: String? = null, +) diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/chat/repository/NetworkChatRepository.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/chat/repository/NetworkChatRepository.kt index 01eb5d3..23c0b15 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/chat/repository/NetworkChatRepository.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/chat/repository/NetworkChatRepository.kt @@ -17,6 +17,8 @@ import ru.daemonlord.messenger.data.chat.dto.ChatMemberDto import ru.daemonlord.messenger.data.chat.dto.ChatMemberRoleUpdateRequestDto import ru.daemonlord.messenger.data.chat.dto.ChatNotificationSettingsDto import ru.daemonlord.messenger.data.chat.dto.ChatNotificationSettingsUpdateDto +import ru.daemonlord.messenger.data.chat.dto.ChatProfileUpdateRequestDto +import ru.daemonlord.messenger.data.chat.dto.ChatTitleUpdateRequestDto import ru.daemonlord.messenger.data.chat.dto.DiscoverChatDto import ru.daemonlord.messenger.data.chat.local.dao.ChatDao import ru.daemonlord.messenger.data.chat.mapper.toChatEntity @@ -225,6 +227,45 @@ class NetworkChatRepository @Inject constructor( } } + override suspend fun updateChatTitle(chatId: Long, title: String): AppResult = withContext(ioDispatcher) { + try { + val updated = chatApiService.updateChatTitle( + chatId = chatId, + request = ChatTitleUpdateRequestDto(title = title), + ) + chatDao.upsertUsers(updated.toUserShortEntityOrNull()?.let(::listOf).orEmpty()) + val entity = updated.toChatEntity() + chatDao.upsertChats(listOf(entity)) + AppResult.Success(entity.toDomain()) + } catch (error: Throwable) { + AppResult.Error(error.toAppError()) + } + } + + override suspend fun updateChatProfile( + chatId: Long, + title: String?, + description: String?, + avatarUrl: String?, + ): AppResult = withContext(ioDispatcher) { + try { + val updated = chatApiService.updateChatProfile( + chatId = chatId, + request = ChatProfileUpdateRequestDto( + title = title, + description = description, + avatarUrl = avatarUrl, + ), + ) + chatDao.upsertUsers(updated.toUserShortEntityOrNull()?.let(::listOf).orEmpty()) + val entity = updated.toChatEntity() + chatDao.upsertChats(listOf(entity)) + AppResult.Success(entity.toDomain()) + } catch (error: Throwable) { + AppResult.Error(error.toAppError()) + } + } + override suspend fun clearChat(chatId: Long): AppResult = withContext(ioDispatcher) { try { chatApiService.clearChat(chatId = chatId) diff --git a/android/app/src/main/java/ru/daemonlord/messenger/domain/chat/repository/ChatRepository.kt b/android/app/src/main/java/ru/daemonlord/messenger/domain/chat/repository/ChatRepository.kt index 132dc2e..d6147a9 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/domain/chat/repository/ChatRepository.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/domain/chat/repository/ChatRepository.kt @@ -32,6 +32,13 @@ interface ChatRepository { suspend fun unarchiveChat(chatId: Long): AppResult suspend fun pinChat(chatId: Long): AppResult suspend fun unpinChat(chatId: Long): AppResult + suspend fun updateChatTitle(chatId: Long, title: String): AppResult + suspend fun updateChatProfile( + chatId: Long, + title: String? = null, + description: String? = null, + avatarUrl: String? = null, + ): AppResult suspend fun clearChat(chatId: Long): AppResult suspend fun removeChat(chatId: Long, forAll: Boolean = false): AppResult suspend fun getChatNotifications(chatId: Long): AppResult diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt index b857a0f..592b46d 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListScreen.kt @@ -130,6 +130,8 @@ fun ChatListRoute( onUnarchiveChat = viewModel::unarchiveChat, onPinChat = viewModel::pinChat, onUnpinChat = viewModel::unpinChat, + onUpdateChatTitle = viewModel::updateChatTitle, + onUpdateChatProfile = viewModel::updateChatProfile, onClearChat = viewModel::clearChat, onDeleteChat = viewModel::deleteChatForMe, onToggleChatMute = viewModel::toggleChatMute, @@ -167,6 +169,8 @@ fun ChatListScreen( onUnarchiveChat: (Long) -> Unit, onPinChat: (Long) -> Unit, onUnpinChat: (Long) -> Unit, + onUpdateChatTitle: (Long, String) -> Unit, + onUpdateChatProfile: (Long, String?, String?) -> Unit, onClearChat: (Long) -> Unit, onDeleteChat: (Long) -> Unit, onToggleChatMute: (Long) -> Unit, @@ -190,6 +194,8 @@ fun ChatListScreen( var createMemberIds by remember { mutableStateOf("") } var createHandle by remember { mutableStateOf("") } var createDescription by remember { mutableStateOf("") } + var manageTitle by remember { mutableStateOf("") } + var manageDescription by remember { mutableStateOf("") } var discoverQuery by remember { mutableStateOf("") } var selectedManageChatIdText by remember { mutableStateOf("") } var manageUserIdText by remember { mutableStateOf("") } @@ -639,6 +645,32 @@ fun ChatListScreen( }, ) { Text("Load members/bans") } if (state.selectedManageChatId != null) { + OutlinedTextField( + value = manageTitle, + onValueChange = { manageTitle = it }, + label = { Text("Manage title") }, + modifier = Modifier.fillMaxWidth(), + ) + OutlinedTextField( + value = manageDescription, + onValueChange = { manageDescription = it }, + label = { Text("Manage description") }, + modifier = Modifier.fillMaxWidth(), + ) + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Button( + onClick = { + val chatId = state.selectedManageChatId ?: return@Button + onUpdateChatTitle(chatId, manageTitle) + }, + ) { Text("Update title") } + Button( + onClick = { + val chatId = state.selectedManageChatId ?: return@Button + onUpdateChatProfile(chatId, manageTitle, manageDescription) + }, + ) { Text("Update profile") } + } Button(onClick = { onCreateInvite(state.selectedManageChatId) }) { Text("Create/regenerate invite") } } diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListViewModel.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListViewModel.kt index d5ebc8e..32a7d9a 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListViewModel.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chats/ChatListViewModel.kt @@ -294,6 +294,42 @@ class ChatListViewModel @Inject constructor( } } + fun updateChatTitle(chatId: Long, title: String) { + val normalized = title.trim() + if (normalized.isBlank()) { + _uiState.update { it.copy(errorMessage = "Title is required.") } + return + } + viewModelScope.launch { + when (val result = chatRepository.updateChatTitle(chatId = chatId, title = normalized)) { + is AppResult.Success -> _uiState.update { it.copy(managementMessage = "Title updated.") } + is AppResult.Error -> _uiState.update { it.copy(errorMessage = result.reason.toUiMessage()) } + } + } + } + + fun updateChatProfile(chatId: Long, title: String?, description: String?) { + val normalizedTitle = title?.trim()?.ifBlank { null } + val normalizedDescription = description?.trim()?.ifBlank { null } + if (normalizedTitle == null && normalizedDescription == null) { + _uiState.update { it.copy(errorMessage = "Provide title or description.") } + return + } + viewModelScope.launch { + when ( + val result = chatRepository.updateChatProfile( + chatId = chatId, + title = normalizedTitle, + description = normalizedDescription, + avatarUrl = null, + ) + ) { + is AppResult.Success -> _uiState.update { it.copy(managementMessage = "Profile updated.") } + is AppResult.Error -> _uiState.update { it.copy(errorMessage = result.reason.toUiMessage()) } + } + } + } + fun deleteChatForMe(chatId: Long) { viewModelScope.launch { when (val result = chatRepository.removeChat(chatId = chatId, forAll = false)) { diff --git a/docs/backend-web-android-parity.md b/docs/backend-web-android-parity.md index 173f23e..babefa4 100644 --- a/docs/backend-web-android-parity.md +++ b/docs/backend-web-android-parity.md @@ -17,8 +17,6 @@ Backend покрывает web-функционал почти полность ## 2) Web endpoints not yet fully used on Android -- `PATCH /api/v1/chats/{chat_id}/title` -- `PATCH /api/v1/chats/{chat_id}/profile` - `GET /api/v1/messages/{message_id}/thread` - `GET /api/v1/search` (single global endpoint; Android uses composed search calls) - Contacts endpoints: @@ -36,9 +34,8 @@ Backend покрывает web-функционал почти полность ## 4) Highest-priority Android parity step -Завершить следующий parity-блок после подключения chat popup/select API: +Завершить следующий parity-блок: -- `GET /api/v1/chats/saved` + UX для Saved -- `PATCH /api/v1/chats/{chat_id}/title` и `/profile` в chat settings flow +- `GET /api/v1/messages/{message_id}/thread` - единый `GET /api/v1/search` для полнофункционального Telegram-like поиска - contacts API (`/users/contacts*`) + экран управления контактами