android: add chat title/profile patch API parity
Some checks failed
Android CI / android (push) Failing after 5m22s
Android Release / release (push) Has started running
CI / test (push) Has been cancelled

This commit is contained in:
Codex
2026-03-09 22:40:52 +03:00
parent 854ba0cbc6
commit 7824ab1a55
8 changed files with 154 additions and 5 deletions

View File

@@ -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).

View File

@@ -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,

View File

@@ -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,
)

View File

@@ -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<ChatItem> = 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<ChatItem> = 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<Unit> = withContext(ioDispatcher) {
try {
chatApiService.clearChat(chatId = chatId)

View File

@@ -32,6 +32,13 @@ interface ChatRepository {
suspend fun unarchiveChat(chatId: Long): AppResult<ChatItem>
suspend fun pinChat(chatId: Long): AppResult<ChatItem>
suspend fun unpinChat(chatId: Long): AppResult<ChatItem>
suspend fun updateChatTitle(chatId: Long, title: String): AppResult<ChatItem>
suspend fun updateChatProfile(
chatId: Long,
title: String? = null,
description: String? = null,
avatarUrl: String? = null,
): AppResult<ChatItem>
suspend fun clearChat(chatId: Long): AppResult<Unit>
suspend fun removeChat(chatId: Long, forAll: Boolean = false): AppResult<Unit>
suspend fun getChatNotifications(chatId: Long): AppResult<ChatNotificationSettings>

View File

@@ -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") }
}

View File

@@ -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)) {