diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 17706a4..3af99c4 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -634,3 +634,8 @@ - Improved `chat_updated` handling in realtime flow: - now refreshes both active and archived chats lists to sync user-scoped flags (`pinned`, `archived`) immediately. - Added parser fallback for realtime chat events to support payloads with either `chat_id` or `id`. + +### Step 99 - Saved chat API parity +- Added Android support for `GET /api/v1/chats/saved`. +- Wired chats overflow `Saved` action to real backend request (instead of local title heuristic). +- Saved chat is now upserted into local Room cache and opened via normal navigation flow. 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 615c152..7471a3d 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 @@ -31,6 +31,9 @@ interface ChatApiService { @Path("chat_id") chatId: Long, ): ChatReadDto + @GET("/api/v1/chats/saved") + suspend fun getSavedChat(): ChatReadDto + @POST("/api/v1/chats/{chat_id}/invite-link") suspend fun createInviteLink( @Path("chat_id") chatId: Long, 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 73e215c..01eb5d3 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 @@ -87,6 +87,18 @@ class NetworkChatRepository @Inject constructor( } } + override suspend fun getSavedChat(): AppResult = withContext(ioDispatcher) { + try { + val chat = chatApiService.getSavedChat() + chatDao.upsertUsers(chat.toUserShortEntityOrNull()?.let(::listOf).orEmpty()) + val entity = chat.toChatEntity() + chatDao.upsertChats(listOf(entity)) + AppResult.Success(entity.toDomain()) + } catch (error: Throwable) { + AppResult.Error(error.toAppError()) + } + } + override suspend fun createInviteLink(chatId: Long): AppResult = withContext(ioDispatcher) { try { AppResult.Success(chatApiService.createInviteLink(chatId = chatId).toDomain()) 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 572f8c9..132dc2e 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 @@ -14,6 +14,7 @@ interface ChatRepository { fun observeChat(chatId: Long): Flow suspend fun refreshChats(archived: Boolean): AppResult suspend fun refreshChat(chatId: Long): AppResult + suspend fun getSavedChat(): AppResult suspend fun createInviteLink(chatId: Long): AppResult suspend fun joinByInvite(token: String): AppResult suspend fun createChat( 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 bfd481c..b857a0f 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 @@ -118,6 +118,7 @@ fun ChatListRoute( onClearSearchHistory = viewModel::clearSearchHistory, onRefresh = viewModel::onPullToRefresh, onOpenChat = onOpenChat, + onOpenSaved = viewModel::openSavedChat, isMainBarVisible = isMainBarVisible, onMainBarVisibilityChanged = onMainBarVisibilityChanged, onCreateGroup = viewModel::createGroup, @@ -154,6 +155,7 @@ fun ChatListScreen( onClearSearchHistory: () -> Unit, onRefresh: () -> Unit, onOpenChat: (Long) -> Unit, + onOpenSaved: () -> Unit, isMainBarVisible: Boolean, onMainBarVisibilityChanged: (Boolean) -> Unit, onCreateGroup: (String, List) -> Unit, @@ -414,15 +416,7 @@ fun ChatListScreen( leadingIcon = { Icon(Icons.Filled.Inventory2, contentDescription = null) }, onClick = { showDefaultMenu = false - val saved = state.chats.firstOrNull { - val title = it.displayTitle.lowercase() - title.contains("saved") || title.contains("избран") - } - if (saved != null) { - onOpenChat(saved.id) - } else { - Toast.makeText(context, "Saved chat not found.", Toast.LENGTH_SHORT).show() - } + onOpenSaved() }, ) DropdownMenuItem( 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 9eb6178..d5ebc8e 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 @@ -150,6 +150,19 @@ class ChatListViewModel @Inject constructor( } } + fun openSavedChat() { + viewModelScope.launch { + when (val result = chatRepository.getSavedChat()) { + is AppResult.Success -> { + _uiState.update { it.copy(pendingOpenChatId = result.data.id) } + } + is AppResult.Error -> _uiState.update { + it.copy(errorMessage = result.reason.toUiMessage()) + } + } + } + } + fun onManagementChatSelected(chatId: Long?) { _uiState.update { it.copy(selectedManageChatId = chatId) } if (chatId != null) { diff --git a/docs/backend-web-android-parity.md b/docs/backend-web-android-parity.md index 077a3aa..173f23e 100644 --- a/docs/backend-web-android-parity.md +++ b/docs/backend-web-android-parity.md @@ -17,7 +17,6 @@ Backend покрывает web-функционал почти полность ## 2) Web endpoints not yet fully used on Android -- `GET /api/v1/chats/saved` - `PATCH /api/v1/chats/{chat_id}/title` - `PATCH /api/v1/chats/{chat_id}/profile` - `GET /api/v1/messages/{message_id}/thread` @@ -33,7 +32,7 @@ Backend покрывает web-функционал почти полность ## 3) Practical status - Backend readiness vs Web: `high` -- Android parity vs Web (feature-level): `~80-85%` +- Android parity vs Web (feature-level): `~82-87%` ## 4) Highest-priority Android parity step