diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md index 93e201e..b491f65 100644 --- a/android/CHANGELOG.md +++ b/android/CHANGELOG.md @@ -162,3 +162,10 @@ - Added safe area insets handling (`WindowInsets.safeDrawing`) for login, chat list, session-check and chat screens. - Added bottom composer protection in chat screen with `navigationBarsPadding()` and `imePadding()`. - Fixed UI overlap with status bar and navigation bar on modern Android devices. + +### Step 26 - Core base / bulk forward foundation +- Added message API/data contracts for bulk forward (`POST /api/v1/messages/{message_id}/forward-bulk`). +- Extended `MessageRepository` with `forwardMessageBulk(...)`. +- Implemented bulk-forward flow in `NetworkMessageRepository` with Room/chat last-message updates. +- Added `ForwardMessageBulkUseCase` for future multi-select message actions. +- Updated message repository unit test fakes to cover new API surface. diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/message/api/MessageApiService.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/message/api/MessageApiService.kt index 68c7c3f..e4344f0 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/message/api/MessageApiService.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/message/api/MessageApiService.kt @@ -9,6 +9,7 @@ import retrofit2.http.Path import retrofit2.http.Query import ru.daemonlord.messenger.data.message.dto.MessageCreateRequestDto import ru.daemonlord.messenger.data.message.dto.MessageForwardRequestDto +import ru.daemonlord.messenger.data.message.dto.MessageForwardBulkRequestDto import ru.daemonlord.messenger.data.message.dto.MessageReadDto import ru.daemonlord.messenger.data.message.dto.MessageReactionDto import ru.daemonlord.messenger.data.message.dto.MessageReactionToggleRequestDto @@ -51,6 +52,12 @@ interface MessageApiService { @Body request: MessageForwardRequestDto, ): MessageReadDto + @POST("/api/v1/messages/{message_id}/forward-bulk") + suspend fun forwardMessageBulk( + @Path("message_id") messageId: Long, + @Body request: MessageForwardBulkRequestDto, + ): List + @GET("/api/v1/messages/{message_id}/reactions") suspend fun listReactions( @Path("message_id") messageId: Long, diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/message/dto/MessageDtos.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/message/dto/MessageDtos.kt index a0ae6c2..7452b99 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/message/dto/MessageDtos.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/message/dto/MessageDtos.kt @@ -60,6 +60,14 @@ data class MessageForwardRequestDto( val includeAuthor: Boolean = true, ) +@Serializable +data class MessageForwardBulkRequestDto( + @SerialName("target_chat_ids") + val targetChatIds: List, + @SerialName("include_author") + val includeAuthor: Boolean = true, +) + @Serializable data class MessageReactionDto( val emoji: String, diff --git a/android/app/src/main/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepository.kt b/android/app/src/main/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepository.kt index 0cc9bda..defffec 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepository.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepository.kt @@ -26,6 +26,7 @@ import ru.daemonlord.messenger.domain.message.model.MessageReaction import ru.daemonlord.messenger.domain.message.repository.MessageRepository import ru.daemonlord.messenger.data.message.dto.MessageCreateRequestDto import ru.daemonlord.messenger.data.message.dto.MessageForwardRequestDto +import ru.daemonlord.messenger.data.message.dto.MessageForwardBulkRequestDto import ru.daemonlord.messenger.data.message.dto.MessageReactionToggleRequestDto import ru.daemonlord.messenger.data.message.dto.MessageStatusUpdateRequestDto import ru.daemonlord.messenger.data.message.dto.MessageUpdateRequestDto @@ -313,6 +314,38 @@ class NetworkMessageRepository @Inject constructor( } } + override suspend fun forwardMessageBulk( + messageId: Long, + targetChatIds: List, + includeAuthor: Boolean, + ): AppResult = withContext(ioDispatcher) { + if (targetChatIds.isEmpty()) return@withContext AppResult.Success(Unit) + try { + val forwarded = messageApiService.forwardMessageBulk( + messageId = messageId, + request = MessageForwardBulkRequestDto( + targetChatIds = targetChatIds, + includeAuthor = includeAuthor, + ), + ) + if (forwarded.isNotEmpty()) { + messageDao.upsertMessages(forwarded.map { it.toEntity() }) + forwarded.forEach { message -> + chatDao.updateLastMessage( + chatId = message.chatId, + lastMessageText = message.text, + lastMessageType = message.type, + lastMessageCreatedAt = message.createdAt, + updatedSortAt = message.createdAt, + ) + } + } + AppResult.Success(Unit) + } catch (error: Throwable) { + AppResult.Error(error.toAppError()) + } + } + override suspend fun listReactions(messageId: Long): AppResult> = withContext(ioDispatcher) { try { val reactions = messageApiService.listReactions(messageId = messageId).map { it.toDomain() } diff --git a/android/app/src/main/java/ru/daemonlord/messenger/domain/message/repository/MessageRepository.kt b/android/app/src/main/java/ru/daemonlord/messenger/domain/message/repository/MessageRepository.kt index 98348a7..3cfa764 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/domain/message/repository/MessageRepository.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/domain/message/repository/MessageRepository.kt @@ -23,6 +23,7 @@ interface MessageRepository { suspend fun markMessageDelivered(chatId: Long, messageId: Long): AppResult suspend fun markMessageRead(chatId: Long, messageId: Long): AppResult suspend fun forwardMessage(messageId: Long, targetChatId: Long, includeAuthor: Boolean = true): AppResult + suspend fun forwardMessageBulk(messageId: Long, targetChatIds: List, includeAuthor: Boolean = true): AppResult suspend fun listReactions(messageId: Long): AppResult> suspend fun toggleReaction(messageId: Long, emoji: String): AppResult> } diff --git a/android/app/src/main/java/ru/daemonlord/messenger/domain/message/usecase/ForwardMessageBulkUseCase.kt b/android/app/src/main/java/ru/daemonlord/messenger/domain/message/usecase/ForwardMessageBulkUseCase.kt new file mode 100644 index 0000000..fb616b3 --- /dev/null +++ b/android/app/src/main/java/ru/daemonlord/messenger/domain/message/usecase/ForwardMessageBulkUseCase.kt @@ -0,0 +1,21 @@ +package ru.daemonlord.messenger.domain.message.usecase + +import ru.daemonlord.messenger.domain.common.AppResult +import ru.daemonlord.messenger.domain.message.repository.MessageRepository +import javax.inject.Inject + +class ForwardMessageBulkUseCase @Inject constructor( + private val repository: MessageRepository, +) { + suspend operator fun invoke( + messageId: Long, + targetChatIds: List, + includeAuthor: Boolean = true, + ): AppResult { + return repository.forwardMessageBulk( + messageId = messageId, + targetChatIds = targetChatIds, + includeAuthor = includeAuthor, + ) + } +} diff --git a/android/app/src/test/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepositoryTest.kt b/android/app/src/test/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepositoryTest.kt index a14dc34..4c46101 100644 --- a/android/app/src/test/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepositoryTest.kt +++ b/android/app/src/test/java/ru/daemonlord/messenger/data/message/repository/NetworkMessageRepositoryTest.kt @@ -26,6 +26,7 @@ import ru.daemonlord.messenger.data.media.dto.UploadUrlResponseDto import ru.daemonlord.messenger.data.message.api.MessageApiService import ru.daemonlord.messenger.data.message.dto.MessageCreateRequestDto import ru.daemonlord.messenger.data.message.dto.MessageForwardRequestDto +import ru.daemonlord.messenger.data.message.dto.MessageForwardBulkRequestDto import ru.daemonlord.messenger.data.message.dto.MessageReadDto import ru.daemonlord.messenger.data.message.dto.MessageReactionDto import ru.daemonlord.messenger.data.message.dto.MessageReactionToggleRequestDto @@ -145,6 +146,18 @@ class NetworkMessageRepositoryTest { return sendResponse.copy(id = messageId + 1000, chatId = request.targetChatId) } + override suspend fun forwardMessageBulk( + messageId: Long, + request: MessageForwardBulkRequestDto, + ): List { + return request.targetChatIds.mapIndexed { index, chatId -> + sendResponse.copy( + id = messageId + 2000 + index, + chatId = chatId, + ) + } + } + override suspend fun listReactions(messageId: Long): List = emptyList() override suspend fun toggleReaction(