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`,
|
- re-encode as `image/jpeg` with quality `82`,
|
||||||
- keep original bytes if compression does not reduce payload size.
|
- 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.
|
- 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.ChatMemberRoleUpdateRequestDto
|
||||||
import ru.daemonlord.messenger.data.chat.dto.ChatNotificationSettingsDto
|
import ru.daemonlord.messenger.data.chat.dto.ChatNotificationSettingsDto
|
||||||
import ru.daemonlord.messenger.data.chat.dto.ChatNotificationSettingsUpdateDto
|
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.ChatReadDto
|
||||||
|
import ru.daemonlord.messenger.data.chat.dto.ChatTitleUpdateRequestDto
|
||||||
import ru.daemonlord.messenger.data.chat.dto.DiscoverChatDto
|
import ru.daemonlord.messenger.data.chat.dto.DiscoverChatDto
|
||||||
|
|
||||||
interface ChatApiService {
|
interface ChatApiService {
|
||||||
@@ -95,6 +97,18 @@ interface ChatApiService {
|
|||||||
@Path("chat_id") chatId: Long,
|
@Path("chat_id") chatId: Long,
|
||||||
): ChatReadDto
|
): 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")
|
@GET("/api/v1/chats/{chat_id}/notifications")
|
||||||
suspend fun getChatNotifications(
|
suspend fun getChatNotifications(
|
||||||
@Path("chat_id") chatId: Long,
|
@Path("chat_id") chatId: Long,
|
||||||
|
|||||||
@@ -132,3 +132,16 @@ data class ChatNotificationSettingsDto(
|
|||||||
data class ChatNotificationSettingsUpdateDto(
|
data class ChatNotificationSettingsUpdateDto(
|
||||||
val muted: Boolean,
|
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.ChatMemberRoleUpdateRequestDto
|
||||||
import ru.daemonlord.messenger.data.chat.dto.ChatNotificationSettingsDto
|
import ru.daemonlord.messenger.data.chat.dto.ChatNotificationSettingsDto
|
||||||
import ru.daemonlord.messenger.data.chat.dto.ChatNotificationSettingsUpdateDto
|
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.dto.DiscoverChatDto
|
||||||
import ru.daemonlord.messenger.data.chat.local.dao.ChatDao
|
import ru.daemonlord.messenger.data.chat.local.dao.ChatDao
|
||||||
import ru.daemonlord.messenger.data.chat.mapper.toChatEntity
|
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) {
|
override suspend fun clearChat(chatId: Long): AppResult<Unit> = withContext(ioDispatcher) {
|
||||||
try {
|
try {
|
||||||
chatApiService.clearChat(chatId = chatId)
|
chatApiService.clearChat(chatId = chatId)
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ interface ChatRepository {
|
|||||||
suspend fun unarchiveChat(chatId: Long): AppResult<ChatItem>
|
suspend fun unarchiveChat(chatId: Long): AppResult<ChatItem>
|
||||||
suspend fun pinChat(chatId: Long): AppResult<ChatItem>
|
suspend fun pinChat(chatId: Long): AppResult<ChatItem>
|
||||||
suspend fun unpinChat(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 clearChat(chatId: Long): AppResult<Unit>
|
||||||
suspend fun removeChat(chatId: Long, forAll: Boolean = false): AppResult<Unit>
|
suspend fun removeChat(chatId: Long, forAll: Boolean = false): AppResult<Unit>
|
||||||
suspend fun getChatNotifications(chatId: Long): AppResult<ChatNotificationSettings>
|
suspend fun getChatNotifications(chatId: Long): AppResult<ChatNotificationSettings>
|
||||||
|
|||||||
@@ -130,6 +130,8 @@ fun ChatListRoute(
|
|||||||
onUnarchiveChat = viewModel::unarchiveChat,
|
onUnarchiveChat = viewModel::unarchiveChat,
|
||||||
onPinChat = viewModel::pinChat,
|
onPinChat = viewModel::pinChat,
|
||||||
onUnpinChat = viewModel::unpinChat,
|
onUnpinChat = viewModel::unpinChat,
|
||||||
|
onUpdateChatTitle = viewModel::updateChatTitle,
|
||||||
|
onUpdateChatProfile = viewModel::updateChatProfile,
|
||||||
onClearChat = viewModel::clearChat,
|
onClearChat = viewModel::clearChat,
|
||||||
onDeleteChat = viewModel::deleteChatForMe,
|
onDeleteChat = viewModel::deleteChatForMe,
|
||||||
onToggleChatMute = viewModel::toggleChatMute,
|
onToggleChatMute = viewModel::toggleChatMute,
|
||||||
@@ -167,6 +169,8 @@ fun ChatListScreen(
|
|||||||
onUnarchiveChat: (Long) -> Unit,
|
onUnarchiveChat: (Long) -> Unit,
|
||||||
onPinChat: (Long) -> Unit,
|
onPinChat: (Long) -> Unit,
|
||||||
onUnpinChat: (Long) -> Unit,
|
onUnpinChat: (Long) -> Unit,
|
||||||
|
onUpdateChatTitle: (Long, String) -> Unit,
|
||||||
|
onUpdateChatProfile: (Long, String?, String?) -> Unit,
|
||||||
onClearChat: (Long) -> Unit,
|
onClearChat: (Long) -> Unit,
|
||||||
onDeleteChat: (Long) -> Unit,
|
onDeleteChat: (Long) -> Unit,
|
||||||
onToggleChatMute: (Long) -> Unit,
|
onToggleChatMute: (Long) -> Unit,
|
||||||
@@ -190,6 +194,8 @@ fun ChatListScreen(
|
|||||||
var createMemberIds by remember { mutableStateOf("") }
|
var createMemberIds by remember { mutableStateOf("") }
|
||||||
var createHandle by remember { mutableStateOf("") }
|
var createHandle by remember { mutableStateOf("") }
|
||||||
var createDescription by remember { mutableStateOf("") }
|
var createDescription by remember { mutableStateOf("") }
|
||||||
|
var manageTitle by remember { mutableStateOf("") }
|
||||||
|
var manageDescription by remember { mutableStateOf("") }
|
||||||
var discoverQuery by remember { mutableStateOf("") }
|
var discoverQuery by remember { mutableStateOf("") }
|
||||||
var selectedManageChatIdText by remember { mutableStateOf("") }
|
var selectedManageChatIdText by remember { mutableStateOf("") }
|
||||||
var manageUserIdText by remember { mutableStateOf("") }
|
var manageUserIdText by remember { mutableStateOf("") }
|
||||||
@@ -639,6 +645,32 @@ fun ChatListScreen(
|
|||||||
},
|
},
|
||||||
) { Text("Load members/bans") }
|
) { Text("Load members/bans") }
|
||||||
if (state.selectedManageChatId != null) {
|
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") }
|
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) {
|
fun deleteChatForMe(chatId: Long) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = chatRepository.removeChat(chatId = chatId, forAll = false)) {
|
when (val result = chatRepository.removeChat(chatId = chatId, forAll = false)) {
|
||||||
|
|||||||
@@ -17,8 +17,6 @@ Backend покрывает web-функционал почти полность
|
|||||||
|
|
||||||
## 2) Web endpoints not yet fully used on Android
|
## 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/messages/{message_id}/thread`
|
||||||
- `GET /api/v1/search` (single global endpoint; Android uses composed search calls)
|
- `GET /api/v1/search` (single global endpoint; Android uses composed search calls)
|
||||||
- Contacts endpoints:
|
- Contacts endpoints:
|
||||||
@@ -36,9 +34,8 @@ Backend покрывает web-функционал почти полность
|
|||||||
|
|
||||||
## 4) Highest-priority Android parity step
|
## 4) Highest-priority Android parity step
|
||||||
|
|
||||||
Завершить следующий parity-блок после подключения chat popup/select API:
|
Завершить следующий parity-блок:
|
||||||
|
|
||||||
- `GET /api/v1/chats/saved` + UX для Saved
|
- `GET /api/v1/messages/{message_id}/thread`
|
||||||
- `PATCH /api/v1/chats/{chat_id}/title` и `/profile` в chat settings flow
|
|
||||||
- единый `GET /api/v1/search` для полнофункционального Telegram-like поиска
|
- единый `GET /api/v1/search` для полнофункционального Telegram-like поиска
|
||||||
- contacts API (`/users/contacts*`) + экран управления контактами
|
- contacts API (`/users/contacts*`) + экран управления контактами
|
||||||
|
|||||||
Reference in New Issue
Block a user