android: add bulk forward core foundation for multi-select
Some checks failed
CI / test (push) Failing after 2m10s

This commit is contained in:
Codex
2026-03-09 13:30:54 +03:00
parent 02ec6c95e9
commit d8916d6738
7 changed files with 90 additions and 0 deletions

View File

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

View File

@@ -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<MessageReadDto>
@GET("/api/v1/messages/{message_id}/reactions")
suspend fun listReactions(
@Path("message_id") messageId: Long,

View File

@@ -60,6 +60,14 @@ data class MessageForwardRequestDto(
val includeAuthor: Boolean = true,
)
@Serializable
data class MessageForwardBulkRequestDto(
@SerialName("target_chat_ids")
val targetChatIds: List<Long>,
@SerialName("include_author")
val includeAuthor: Boolean = true,
)
@Serializable
data class MessageReactionDto(
val emoji: String,

View File

@@ -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<Long>,
includeAuthor: Boolean,
): AppResult<Unit> = 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<List<MessageReaction>> = withContext(ioDispatcher) {
try {
val reactions = messageApiService.listReactions(messageId = messageId).map { it.toDomain() }

View File

@@ -23,6 +23,7 @@ interface MessageRepository {
suspend fun markMessageDelivered(chatId: Long, messageId: Long): AppResult<Unit>
suspend fun markMessageRead(chatId: Long, messageId: Long): AppResult<Unit>
suspend fun forwardMessage(messageId: Long, targetChatId: Long, includeAuthor: Boolean = true): AppResult<Unit>
suspend fun forwardMessageBulk(messageId: Long, targetChatIds: List<Long>, includeAuthor: Boolean = true): AppResult<Unit>
suspend fun listReactions(messageId: Long): AppResult<List<MessageReaction>>
suspend fun toggleReaction(messageId: Long, emoji: String): AppResult<List<MessageReaction>>
}

View File

@@ -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<Long>,
includeAuthor: Boolean = true,
): AppResult<Unit> {
return repository.forwardMessageBulk(
messageId = messageId,
targetChatIds = targetChatIds,
includeAuthor = includeAuthor,
)
}
}

View File

@@ -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<MessageReadDto> {
return request.targetChatIds.mapIndexed { index, chatId ->
sendResponse.copy(
id = messageId + 2000 + index,
chatId = chatId,
)
}
}
override suspend fun listReactions(messageId: Long): List<MessageReactionDto> = emptyList()
override suspend fun toggleReaction(