android: add chat title/profile patch API parity
This commit is contained in:
@@ -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).
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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") }
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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*`) + экран управления контактами
|
||||
|
||||
Reference in New Issue
Block a user