[wip] chat materials; some experiments with local composition and blur

This commit is contained in:
2024-07-12 00:51:24 +03:00
parent c43278e4cf
commit fb76b46b22
46 changed files with 1210 additions and 717 deletions
@@ -9,6 +9,7 @@ import androidx.compose.animation.slideOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Settings
@@ -16,19 +17,23 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarDefaults
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
@@ -36,9 +41,16 @@ import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import com.meloda.app.fast.conversations.navigation.Conversations
import com.meloda.app.fast.conversations.navigation.conversationsRoute
import com.meloda.app.fast.designsystem.LocalBottomPadding
import com.meloda.app.fast.designsystem.LocalHazeState
import com.meloda.app.fast.designsystem.LocalTheme
import com.meloda.app.fast.friends.navigation.Friends
import com.meloda.app.fast.friends.navigation.friendsRoute
import com.meloda.app.fast.model.BaseError
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import kotlinx.serialization.Serializable
import com.meloda.app.fast.designsystem.R as UiR
@@ -58,7 +70,7 @@ data class BottomNavigationItem(
val route: Any,
)
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
fun NavGraphBuilder.mainScreen(
onError: (BaseError) -> Unit,
onNavigateToSettings: () -> Unit,
@@ -87,6 +99,8 @@ fun NavGraphBuilder.mainScreen(
val routes = items.map(BottomNavigationItem::route)
composable<Main> {
val currentTheme = LocalTheme.current
val hazeState = remember { HazeState() }
val navController = rememberNavController()
var selectedItemIndex by rememberSaveable {
@@ -104,7 +118,21 @@ fun NavGraphBuilder.mainScreen(
enter = slideIn { IntOffset(0, 400) },
exit = slideOut { IntOffset(0, 400) }
) {
NavigationBar {
NavigationBar(
modifier = Modifier
.then(
if (currentTheme.usingBlur) {
Modifier.hazeChild(
state = hazeState,
style = HazeMaterials.thick()
)
} else Modifier
)
.fillMaxWidth(),
containerColor = NavigationBarDefaults.containerColor.copy(
alpha = if (currentTheme.usingBlur) 0f else 1f
)
) {
items.forEachIndexed { index, item ->
NavigationBarItem(
selected = selectedItemIndex == index,
@@ -139,7 +167,11 @@ fun NavGraphBuilder.mainScreen(
Box(
modifier = Modifier
.fillMaxSize()
.padding(bottom = padding.calculateBottomPadding())
.padding(bottom = if (currentTheme.usingBlur) 0.dp else padding.calculateBottomPadding())
) {
CompositionLocalProvider(
LocalHazeState provides hazeState,
LocalBottomPadding provides if (currentTheme.usingBlur) padding.calculateBottomPadding() else 0.dp
) {
NavHost(
navController = navController,
@@ -193,4 +225,5 @@ fun NavGraphBuilder.mainScreen(
}
}
}
}
}
@@ -6,6 +6,7 @@ import android.os.PowerManager
import androidx.preference.PreferenceManager
import com.meloda.app.fast.MainViewModelImpl
import com.meloda.app.fast.auth.authModule
import com.meloda.app.fast.chatmaterials.di.chatMaterialsModule
import com.meloda.app.fast.conversations.di.conversationsModule
import com.meloda.app.fast.data.di.dataModule
import com.meloda.app.fast.friends.di.friendsModule
@@ -32,7 +33,8 @@ val applicationModule = module {
languagePickerModule,
longPollModule,
friendsModule,
profileModule
profileModule,
chatMaterialsModule
)
// TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors
@@ -231,13 +231,13 @@ class LongPollUpdatesParser(
Log.d("LongPollUpdatesParser", "$eventType: $event")
}
private suspend fun <T : LongPollEvent> loadNormalMessage(
private suspend inline fun <reified T : LongPollEvent> loadNormalMessage(
eventType: ApiEvent,
messageId: Int
): T? = suspendCoroutine {
): T? = suspendCoroutine { continuation ->
coroutineScope.launch(Dispatchers.IO) {
messagesUseCase.getById(
messageId = messageId,
messageIds = listOf(messageId),
extended = true,
fields = VkConstants.ALL_FIELDS
).listenValue(this) { state ->
@@ -245,8 +245,12 @@ class LongPollUpdatesParser(
error = { error ->
Log.e("LongPollUpdatesParser", "loadNormalMessage: error: $error")
},
success = { response ->
response?.let { message ->
success = { messages ->
val message = messages.singleOrNull() ?: run {
continuation.resume(null)
return@listenValue
}
VkMemoryCache[message.id] = message
messagesUseCase.storeMessage(message)
@@ -254,11 +258,13 @@ class LongPollUpdatesParser(
ApiEvent.MESSAGE_NEW -> LongPollEvent.VkMessageNewEvent(message)
ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(message)
else -> null
else -> {
continuation.resume(null)
null
}
}
resumeValue?.let { value -> it.resume(value as T) }
} ?: it.resume(null)
resumeValue?.let { value -> continuation.resume(value as T) }
}
)
}
@@ -0,0 +1,9 @@
package com.meloda.app.fast.data.api.messages
import com.meloda.app.fast.model.api.domain.VkConversation
import com.meloda.app.fast.model.api.domain.VkMessage
data class MessagesHistoryInfo(
val messages: List<VkMessage>,
val conversations: List<VkConversation>
)
@@ -1,16 +0,0 @@
package com.meloda.app.fast.data.api.messages
import com.meloda.app.fast.model.database.VkMessageEntity
interface MessagesLocalDataSource {
suspend fun getMessages(
conversationId: Int,
offset: Int?,
count: Int?
): List<VkMessageEntity>
suspend fun getMessage(messageId: Int): VkMessageEntity?
suspend fun storeMessages(messages: List<VkMessageEntity>)
}
@@ -1,24 +0,0 @@
package com.meloda.app.fast.data.api.messages
import com.meloda.app.fast.database.dao.MessageDao
import com.meloda.app.fast.model.database.VkMessageEntity
// TODO: 05/05/2024, Danil Nikolaev: use paging for room
class MessagesLocalDataSourceImpl(
private val messageDao: MessageDao
) : MessagesLocalDataSource {
override suspend fun getMessages(
conversationId: Int,
offset: Int?,
count: Int?
): List<VkMessageEntity> = messageDao.getAll(conversationId)
override suspend fun getMessage(
messageId: Int
): VkMessageEntity? = messageDao.getById(messageId)
override suspend fun storeMessages(messages: List<VkMessageEntity>) {
messageDao.insertAll(messages)
}
}
@@ -1,36 +0,0 @@
package com.meloda.app.fast.data.api.messages
import com.meloda.app.fast.model.api.domain.VkAttachment
import com.meloda.app.fast.model.api.domain.VkMessage
import com.meloda.app.fast.network.RestApiErrorDomain
import com.slack.eithernet.ApiResult
interface MessagesNetworkDataSource {
suspend fun getMessagesHistory(
conversationId: Int,
offset: Int?,
count: Int?,
): ApiResult<MessagesHistoryDomain, RestApiErrorDomain>
suspend fun getMessageById(
messagesIds: List<Int>,
extended: Boolean?,
fields: String?
): ApiResult<VkMessage, RestApiErrorDomain>
suspend fun send(
peerId: Int,
randomId: Int,
message: String?,
replyTo: Int?,
attachments: List<VkAttachment>?
): ApiResult<Int, RestApiErrorDomain>
suspend fun markAsRead(
peerId: Int,
startMessageId: Int?
): ApiResult<Int, RestApiErrorDomain>
suspend fun getMessage(messageId: Int): VkMessage?
}
@@ -1,164 +0,0 @@
package com.meloda.app.fast.data.api.messages
import com.meloda.app.fast.common.VkConstants
import com.meloda.app.fast.data.VkGroupsMap
import com.meloda.app.fast.data.VkMemoryCache
import com.meloda.app.fast.data.VkUsersMap
import com.meloda.app.fast.model.api.data.VkContactData
import com.meloda.app.fast.model.api.data.VkGroupData
import com.meloda.app.fast.model.api.data.VkUserData
import com.meloda.app.fast.model.api.data.asDomain
import com.meloda.app.fast.model.api.domain.VkAttachment
import com.meloda.app.fast.model.api.domain.VkConversation
import com.meloda.app.fast.model.api.domain.VkMessage
import com.meloda.app.fast.model.api.requests.MessagesGetByIdRequest
import com.meloda.app.fast.model.api.requests.MessagesGetHistoryRequest
import com.meloda.app.fast.model.api.requests.MessagesMarkAsReadRequest
import com.meloda.app.fast.model.api.requests.MessagesSendRequest
import com.meloda.app.fast.network.RestApiErrorDomain
import com.meloda.app.fast.network.mapApiDefault
import com.meloda.app.fast.network.mapApiResult
import com.meloda.app.fast.network.service.messages.MessagesService
import com.slack.eithernet.ApiResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class MessagesNetworkDataSourceImpl(
private val messagesService: MessagesService
) : MessagesNetworkDataSource {
override suspend fun getMessagesHistory(
conversationId: Int,
offset: Int?,
count: Int?
): ApiResult<MessagesHistoryDomain, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesGetHistoryRequest(
count = count,
offset = offset,
peerId = conversationId,
extended = true,
startMessageId = null,
rev = null,
fields = VkConstants.ALL_FIELDS
)
messagesService.getHistory(requestModel.map).mapApiResult(
successMapper = { apiResponse ->
val response = apiResponse.requireResponse()
val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain)
val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain)
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
val usersMap = VkUsersMap.forUsers(profilesList)
val groupsMap = VkGroupsMap.forGroups(groupsList)
VkMemoryCache.appendUsers(profilesList)
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
val messages = response.items.map { item ->
item.asDomain().let { message ->
message.copy(
user = usersMap.messageUser(message),
group = groupsMap.messageGroup(message),
actionUser = usersMap.messageActionUser(message),
actionGroup = groupsMap.messageActionGroup(message)
).also { VkMemoryCache[message.id] = it }
}
}
val conversations = response.conversations.orEmpty().map { item ->
val message = messages.firstOrNull { it.id == item.lastMessageId }
item.asDomain(message)
.let { conversation ->
conversation.copy(
user = usersMap.conversationUser(conversation),
group = groupsMap.conversationGroup(conversation)
).also { VkMemoryCache[conversation.id] = it }
}
}
MessagesHistoryDomain(
messages = messages,
conversations = conversations
)
},
errorMapper = { error ->
error?.toDomain()
}
)
}
override suspend fun getMessageById(
messagesIds: List<Int>,
extended: Boolean?,
fields: String?
): ApiResult<VkMessage, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesGetByIdRequest(
messagesIds = messagesIds,
extended = extended,
fields = fields
)
messagesService.getById(requestModel.map).mapApiResult(
successMapper = { apiResponse ->
val response = apiResponse.requireResponse()
val message = response.items.single()
val usersMap =
VkUsersMap.forUsers(response.profiles.orEmpty().map(VkUserData::mapToDomain))
val groupsMap =
VkGroupsMap.forGroups(response.groups.orEmpty().map(VkGroupData::mapToDomain))
message.asDomain().copy(
user = usersMap.messageUser(message),
group = groupsMap.messageGroup(message),
actionUser = usersMap.messageActionUser(message),
actionGroup = groupsMap.messageActionGroup(message)
)
},
errorMapper = { error -> error?.toDomain() }
)
}
override suspend fun send(
peerId: Int,
randomId: Int,
message: String?,
replyTo: Int?,
attachments: List<VkAttachment>?
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesSendRequest(
peerId = peerId,
randomId = randomId,
message = message,
replyTo = replyTo,
attachments = attachments
)
messagesService.send(requestModel.map).mapApiDefault()
}
override suspend fun markAsRead(
peerId: Int,
startMessageId: Int?
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesMarkAsReadRequest(
peerId = peerId,
startMessageId = startMessageId
)
messagesService.markAsRead(requestModel.map).mapApiDefault()
}
override suspend fun getMessage(messageId: Int): VkMessage? = withContext(Dispatchers.IO) {
// TODO: 05/05/2024, Danil Nikolaev: get message
null
}
}
data class MessagesHistoryDomain(
val messages: List<VkMessage>,
val conversations: List<VkConversation>
)
@@ -1,24 +1,24 @@
package com.meloda.app.fast.data.api.messages
import com.meloda.app.fast.model.api.domain.VkAttachment
import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage
import com.meloda.app.fast.model.api.domain.VkMessage
import com.meloda.app.fast.network.RestApiErrorDomain
import com.slack.eithernet.ApiResult
import kotlinx.coroutines.flow.Flow
interface MessagesRepository {
suspend fun getMessagesHistory(
suspend fun getHistory(
conversationId: Int,
offset: Int?,
count: Int?
): ApiResult<MessagesHistoryDomain, RestApiErrorDomain>
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain>
suspend fun getMessageById(
suspend fun getById(
messagesIds: List<Int>,
extended: Boolean?,
fields: String?
): ApiResult<VkMessage, RestApiErrorDomain>
): ApiResult<List<VkMessage>, RestApiErrorDomain>
suspend fun send(
peerId: Int,
@@ -33,14 +33,16 @@ interface MessagesRepository {
startMessageId: Int?
): ApiResult<Int, RestApiErrorDomain>
suspend fun getMessage(messageId: Int): Flow<VkMessage?>
suspend fun getHistoryAttachments(
peerId: Int,
count: Int?,
offset: Int?,
attachmentTypes: List<String>,
conversationMessageId: Int
): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain>
suspend fun storeMessages(messages: List<VkMessage>)
// suspend fun getHistory(
// params: MessagesGetHistoryRequest
// ): ApiResult<MessagesGetHistoryResponse, RestApiErrorDomain>
// suspend fun markAsImportant(
// params: MessagesMarkAsImportantRequest
// ): ApiResult<List<Int>, RestApiErrorDomain>
@@ -1,56 +1,132 @@
package com.meloda.app.fast.data.api.messages
import com.meloda.app.fast.common.VkConstants
import com.meloda.app.fast.data.VkGroupsMap
import com.meloda.app.fast.data.VkMemoryCache
import com.meloda.app.fast.data.VkUsersMap
import com.meloda.app.fast.database.dao.MessageDao
import com.meloda.app.fast.model.api.data.VkAttachmentHistoryMessageData
import com.meloda.app.fast.model.api.data.VkContactData
import com.meloda.app.fast.model.api.data.VkGroupData
import com.meloda.app.fast.model.api.data.VkUserData
import com.meloda.app.fast.model.api.data.asDomain
import com.meloda.app.fast.model.api.domain.VkAttachment
import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage
import com.meloda.app.fast.model.api.domain.VkMessage
import com.meloda.app.fast.model.api.domain.asEntity
import com.meloda.app.fast.model.database.asExternalModel
import com.meloda.app.fast.model.api.requests.MessagesGetByIdRequest
import com.meloda.app.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest
import com.meloda.app.fast.model.api.requests.MessagesGetHistoryRequest
import com.meloda.app.fast.model.api.requests.MessagesMarkAsReadRequest
import com.meloda.app.fast.model.api.requests.MessagesSendRequest
import com.meloda.app.fast.network.RestApiErrorDomain
import com.meloda.app.fast.network.mapApiDefault
import com.meloda.app.fast.network.mapApiResult
import com.meloda.app.fast.network.service.messages.MessagesService
import com.slack.eithernet.ApiResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext
// TODO: 05/05/2024, Danil Nikolaev: implement syncing
class MessagesRepositoryImpl(
private val networkDataSource: MessagesNetworkDataSource,
private val localDataSource: MessagesLocalDataSource
private val messagesService: MessagesService,
private val messageDao: MessageDao,
) : MessagesRepository {
override suspend fun getMessagesHistory(
override suspend fun getHistory(
conversationId: Int,
offset: Int?,
count: Int?
): ApiResult<MessagesHistoryDomain, RestApiErrorDomain> = withContext(Dispatchers.IO) {
// val localMessages = localDataSource.getMessages(
// conversationId = conversationId,
// offset = offset,
// count = count
// ).map(VkMessageEntity::asExternalModel)
//
// emit(localMessages)
//
// val networkMessages = networkDataSource.getMessagesHistory(
// conversationId = conversationId,
// offset = offset,
// count = count
// )
//
// emit(networkMessages)
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesGetHistoryRequest(
count = count,
offset = offset,
peerId = conversationId,
extended = true,
startMessageId = null,
rev = null,
fields = VkConstants.ALL_FIELDS
)
networkDataSource.getMessagesHistory(conversationId, offset, count)
messagesService.getHistory(requestModel.map).mapApiResult(
successMapper = { apiResponse ->
val response = apiResponse.requireResponse()
val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain)
val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain)
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
val usersMap = VkUsersMap.forUsers(profilesList)
val groupsMap = VkGroupsMap.forGroups(groupsList)
VkMemoryCache.appendUsers(profilesList)
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
val messages = response.items.map { item ->
item.asDomain().let { message ->
message.copy(
user = usersMap.messageUser(message),
group = groupsMap.messageGroup(message),
actionUser = usersMap.messageActionUser(message),
actionGroup = groupsMap.messageActionGroup(message)
).also { VkMemoryCache[message.id] = it }
}
}
override suspend fun getMessageById(
val conversations = response.conversations.orEmpty().map { item ->
val message = messages.firstOrNull { it.id == item.lastMessageId }
item.asDomain(message)
.let { conversation ->
conversation.copy(
user = usersMap.conversationUser(conversation),
group = groupsMap.conversationGroup(conversation)
).also { VkMemoryCache[conversation.id] = it }
}
}
MessagesHistoryInfo(
messages = messages,
conversations = conversations
)
},
errorMapper = { error ->
error?.toDomain()
}
)
}
override suspend fun getById(
messagesIds: List<Int>,
extended: Boolean?,
fields: String?
): ApiResult<VkMessage, RestApiErrorDomain> = withContext(Dispatchers.IO) {
networkDataSource.getMessageById(
): ApiResult<List<VkMessage>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesGetByIdRequest(
messagesIds = messagesIds,
extended = extended,
fields = fields
)
messagesService.getById(requestModel.map).mapApiResult(
successMapper = { apiResponse ->
val response = apiResponse.requireResponse()
val messages = response.items
val usersMap =
VkUsersMap.forUsers(response.profiles.orEmpty().map(VkUserData::mapToDomain))
val groupsMap =
VkGroupsMap.forGroups(response.groups.orEmpty().map(VkGroupData::mapToDomain))
messages.map { message ->
message.asDomain().copy(
user = usersMap.messageUser(message),
group = groupsMap.messageGroup(message),
actionUser = usersMap.messageActionUser(message),
actionGroup = groupsMap.messageActionGroup(message)
)
}
},
errorMapper = { error -> error?.toDomain() }
)
}
override suspend fun send(
@@ -60,54 +136,72 @@ class MessagesRepositoryImpl(
replyTo: Int?,
attachments: List<VkAttachment>?
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
networkDataSource.send(
peerId,
randomId,
message,
replyTo,
attachments
val requestModel = MessagesSendRequest(
peerId = peerId,
randomId = randomId,
message = message,
replyTo = replyTo,
attachments = attachments
)
messagesService.send(requestModel.map).mapApiDefault()
}
override suspend fun markAsRead(
peerId: Int,
startMessageId: Int?
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
networkDataSource.markAsRead(peerId, startMessageId)
val requestModel = MessagesMarkAsReadRequest(
peerId = peerId,
startMessageId = startMessageId
)
messagesService.markAsRead(requestModel.map).mapApiDefault()
}
override suspend fun getMessage(messageId: Int): Flow<VkMessage?> = flow {
val localMessage = localDataSource.getMessage(messageId)?.asExternalModel()
override suspend fun getHistoryAttachments(
peerId: Int,
count: Int?,
offset: Int?,
attachmentTypes: List<String>,
conversationMessageId: Int
): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain> =
withContext(Dispatchers.IO) {
val requestModel = MessagesGetHistoryAttachmentsRequest(
peerId = peerId,
extended = true,
count = count,
offset = offset,
preserveOrder = true,
attachmentTypes = attachmentTypes,
conversationMessageId = conversationMessageId,
fields = VkConstants.ALL_FIELDS
)
emit(localMessage)
messagesService.getHistoryAttachments(requestModel.map).mapApiResult(
successMapper = { apiResponse ->
val response = apiResponse.requireResponse()
val networkMessage = networkDataSource.getMessage(messageId)
val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain)
val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain)
val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain)
emit(networkMessage)
VkMemoryCache.appendUsers(profilesList)
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
response.items.map(VkAttachmentHistoryMessageData::toDomain)
},
errorMapper = { error ->
error?.toDomain()
}
)
}
override suspend fun storeMessages(messages: List<VkMessage>) {
localDataSource.storeMessages(messages.map(VkMessage::asEntity))
messageDao.insertAll(messages.map(VkMessage::asEntity))
}
// override suspend fun getHistory(
// params: MessagesGetHistoryRequest
// ): ApiResult<MessagesGetHistoryResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
// messagesService.getHistory(params.map).mapResult(
// successMapper = { response -> response.requireResponse() },
// errorMapper = { error -> error?.toDomain() }
// )
// }
//
// override suspend fun send(
// params: MessagesSendRequest
// ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
// messagesService.send(params.map).mapResult(
// successMapper = { response -> response.requireResponse() },
// errorMapper = { error -> error?.toDomain() }
// )
// }
//
// override suspend fun markAsImportant(
// params: MessagesMarkAsImportantRequest
// ): ApiResult<List<Int>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
@@ -153,24 +247,6 @@ class MessagesRepositoryImpl(
// )
// }
//
// override suspend fun getById(
// params: MessagesGetByIdRequest
// ): ApiResult<MessagesGetByIdResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
// messagesService.getById(params.map).mapResult(
// successMapper = { response -> response.requireResponse() },
// errorMapper = { error -> error?.toDomain() }
// )
// }
//
// override suspend fun markAsRead(
// params: MessagesMarkAsReadRequest
// ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
// messagesService.markAsRead(params.map).mapResult(
// successMapper = { response -> response.requireResponse() },
// errorMapper = { error -> error?.toDomain() }
// )
// }
//
// override suspend fun getChat(
// params: MessagesGetChatRequest
// ): ApiResult<VkChatData, RestApiErrorDomain> = withContext(Dispatchers.IO) {
@@ -199,3 +275,4 @@ class MessagesRepositoryImpl(
// )
// }
}
@@ -2,6 +2,7 @@ package com.meloda.app.fast.data.api.messages
import com.meloda.app.fast.data.State
import com.meloda.app.fast.model.api.domain.VkAttachment
import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage
import com.meloda.app.fast.model.api.domain.VkMessage
import kotlinx.coroutines.flow.Flow
@@ -11,15 +12,9 @@ interface MessagesUseCase {
conversationId: Int,
count: Int?,
offset: Int?
): Flow<State<MessagesHistoryDomain>>
): Flow<State<MessagesHistoryInfo>>
fun getById(
messageId: Int,
extended: Boolean?,
fields: String?
): Flow<State<VkMessage?>>
fun getByIds(
messageIds: List<Int>,
extended: Boolean?,
fields: String?
@@ -38,6 +33,14 @@ interface MessagesUseCase {
startMessageId: Int
): Flow<State<Int>>
fun getHistoryAttachments(
peerId: Int,
count: Int?,
offset: Int?,
attachmentTypes: List<String>,
conversationMessageId: Int
): Flow<State<List<VkAttachmentHistoryMessage>>>
suspend fun storeMessage(message: VkMessage)
suspend fun storeMessages(messages: List<VkMessage>)
}
@@ -15,10 +15,6 @@ import com.meloda.app.fast.data.api.friends.FriendsRepository
import com.meloda.app.fast.data.api.friends.FriendsRepositoryImpl
import com.meloda.app.fast.data.api.longpoll.LongPollRepository
import com.meloda.app.fast.data.api.longpoll.LongPollRepositoryImpl
import com.meloda.app.fast.data.api.messages.MessagesLocalDataSource
import com.meloda.app.fast.data.api.messages.MessagesLocalDataSourceImpl
import com.meloda.app.fast.data.api.messages.MessagesNetworkDataSource
import com.meloda.app.fast.data.api.messages.MessagesNetworkDataSourceImpl
import com.meloda.app.fast.data.api.messages.MessagesRepository
import com.meloda.app.fast.data.api.messages.MessagesRepositoryImpl
import com.meloda.app.fast.data.api.oauth.OAuthRepository
@@ -59,8 +55,6 @@ val dataModule = module {
singleOf(::LongPollRepositoryImpl) bind LongPollRepository::class
singleOf(::MessagesLocalDataSourceImpl) bind MessagesLocalDataSource::class
singleOf(::MessagesNetworkDataSourceImpl) bind MessagesNetworkDataSource::class
singleOf(::MessagesRepositoryImpl) bind MessagesRepository::class
singleOf(::OAuthRepositoryImpl) bind OAuthRepository::class
@@ -21,7 +21,7 @@ import com.meloda.app.fast.model.database.VkUserEntity
VkConversationEntity::class
],
version = 5
version = 6
)
@TypeConverters(Converters::class)
abstract class CacheDatabase : RoomDatabase() {
@@ -18,12 +18,14 @@ import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import com.meloda.app.fast.datastore.isUsingAmoledBackground
import com.meloda.app.fast.datastore.isUsingDynamicColors
import com.meloda.app.fast.datastore.model.ThemeConfig
import com.meloda.app.fast.datastore.selectedColorScheme
import com.meloda.app.fast.designsystem.colorschemes.ClassicColorScheme
import dev.chrisbanes.haze.HazeState
private val googleSansFonts = FontFamily(
Font(resId = R.font.google_sans_regular),
@@ -115,6 +117,14 @@ val LocalTheme = compositionLocalOf {
)
}
val LocalHazeState = compositionLocalOf {
HazeState()
}
val LocalBottomPadding = compositionLocalOf {
0.dp
}
@Composable
fun AppTheme(
predefinedColorScheme: ColorScheme? = null,
@@ -0,0 +1,25 @@
package com.meloda.app.fast.model.api.data
import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class VkAttachmentHistoryMessageData(
@Json(name = "message_id") val messageId: Int,
@Json(name = "date") val date: Int,
@Json(name = "cmid") val conversationMessageId: Int,
@Json(name = "from_id") val fromId: Int,
@Json(name = "position") val position: Int,
@Json(name = "attachment") val attachment: VkAttachmentItemData
) {
fun toDomain(): VkAttachmentHistoryMessage = VkAttachmentHistoryMessage(
messageId = messageId,
conversationMessageId = conversationMessageId,
date = date,
fromId = fromId,
position = position,
attachment = attachment.toDomain()
)
}
@@ -1,5 +1,7 @@
package com.meloda.app.fast.model.api.data
import com.meloda.app.fast.model.api.domain.VkAttachment
import com.meloda.app.fast.model.api.domain.VkUnknownAttachment
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@@ -12,7 +14,7 @@ data class VkAttachmentItemData(
@Json(name = "doc") val file: VkFileData?,
@Json(name = "link") val link: VkLinkData?,
@Json(name = "mini_app") val miniApp: VkMiniAppData?,
@Json(name = "audio_message") val voiceMessage: VkAudioMessageData?,
@Json(name = "audio_message") val audioMessage: VkAudioMessageData?,
@Json(name = "sticker") val sticker: VkStickerData?,
@Json(name = "gift") val gift: VkGiftData?,
@Json(name = "wall") val wall: VkWallData?,
@@ -20,7 +22,7 @@ data class VkAttachmentItemData(
@Json(name = "poll") val poll: VkPollData?,
@Json(name = "wall_reply") val wallReply: VkWallReplyData?,
@Json(name = "call") val call: VkCallData?,
@Json(name = "group_call_in_progress") val groupCall: VkGroupCallData?,
@Json(name = "group_call_in_progress") val groupCallInProgress: VkGroupCallData?,
@Json(name = "curator") val curator: VkCuratorData?,
@Json(name = "event") val event: VkEventData?,
@Json(name = "story") val story: VkStoryData?,
@@ -30,6 +32,29 @@ data class VkAttachmentItemData(
@Json(name = "audio_playlist") val audioPlaylist: VkAudioPlaylistData?,
@Json(name = "podcast") val podcast: VkPodcastData?
) {
fun getPreparedType(): AttachmentType = AttachmentType.parse(type)
fun toDomain(): VkAttachment = when (AttachmentType.parse(type)) {
AttachmentType.UNKNOWN -> VkUnknownAttachment
AttachmentType.PHOTO -> photo?.toDomain()
AttachmentType.VIDEO -> video?.toDomain()
AttachmentType.AUDIO -> audio?.toDomain()
AttachmentType.FILE -> file?.toDomain()
AttachmentType.LINK -> link?.toDomain()
AttachmentType.MINI_APP -> miniApp?.toDomain()
AttachmentType.AUDIO_MESSAGE -> audioMessage?.toDomain()
AttachmentType.STICKER -> sticker?.toDomain()
AttachmentType.GIFT -> gift?.toDomain()
AttachmentType.WALL -> wall?.toDomain()
AttachmentType.GRAFFITI -> graffiti?.toDomain()
AttachmentType.POLL -> poll?.toDomain()
AttachmentType.WALL_REPLY -> wallReply?.toDomain()
AttachmentType.CALL -> call?.toDomain()
AttachmentType.GROUP_CALL_IN_PROGRESS -> groupCallInProgress?.toDomain()
AttachmentType.CURATOR -> curator?.toDomain()
AttachmentType.EVENT -> event?.toDomain()
AttachmentType.STORY -> story?.toDomain()
AttachmentType.WIDGET -> widget?.toDomain()
AttachmentType.ARTIST -> artist?.toDomain()
AttachmentType.AUDIO_PLAYLIST -> audioPlaylist?.toDomain()
AttachmentType.PODCAST -> podcast?.toDomain()
} ?: VkUnknownAttachment
}
@@ -1,6 +1,5 @@
package com.meloda.app.fast.model.api.data
import com.meloda.app.fast.model.api.domain.VkAttachment
import com.meloda.app.fast.model.api.domain.VkMessage
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@@ -60,6 +59,7 @@ data class VkMessageData(
fun VkMessageData.asDomain(): VkMessage = VkMessage(
id = id ?: -1,
conversationMessageId = conversationMessageId,
text = text.ifBlank { null },
isOut = out == 1,
peerId = peerId ?: -1,
@@ -75,134 +75,10 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage(
important = important,
updateTime = updateTime,
forwards = fwdMessages.orEmpty().map(VkMessageData::asDomain),
attachments = parseAttachments(),
attachments = attachments.map(VkAttachmentItemData::toDomain),
replyMessage = replyMessage?.asDomain(),
user = null,
group = null,
actionUser = null,
actionGroup = null,
)
private fun VkMessageData.parseAttachments(): List<VkAttachment> {
if (attachments.isEmpty()) return emptyList()
val attachments = mutableListOf<VkAttachment>()
for (baseAttachment in this.attachments) {
when (baseAttachment.getPreparedType()) {
AttachmentType.UNKNOWN -> continue
AttachmentType.PHOTO -> {
val photo = baseAttachment.photo ?: continue
attachments += photo.toDomain()
}
AttachmentType.VIDEO -> {
val video = baseAttachment.video ?: continue
attachments += video.toDomain()
}
AttachmentType.AUDIO -> {
val audio = baseAttachment.audio ?: continue
attachments += audio.toDomain()
}
AttachmentType.FILE -> {
val file = baseAttachment.file ?: continue
attachments += file.toDomain()
}
AttachmentType.LINK -> {
val link = baseAttachment.link ?: continue
attachments += link.toDomain()
}
AttachmentType.MINI_APP -> {
val miniApp = baseAttachment.miniApp ?: continue
attachments += miniApp.toDomain()
}
AttachmentType.AUDIO_MESSAGE -> {
val voiceMessage = baseAttachment.voiceMessage ?: continue
attachments += voiceMessage.toDomain()
}
AttachmentType.STICKER -> {
val sticker = baseAttachment.sticker ?: continue
attachments += sticker.toDomain()
}
AttachmentType.GIFT -> {
val gift = baseAttachment.gift ?: continue
attachments += gift.toDomain()
}
AttachmentType.WALL -> {
val wall = baseAttachment.wall ?: continue
attachments += wall.toDomain()
}
AttachmentType.GRAFFITI -> {
val graffiti = baseAttachment.graffiti ?: continue
attachments += graffiti.toDomain()
}
AttachmentType.POLL -> {
val poll = baseAttachment.poll ?: continue
attachments += poll.toDomain()
}
AttachmentType.WALL_REPLY -> {
val wallReply = baseAttachment.wallReply ?: continue
attachments += wallReply.toDomain()
}
AttachmentType.CALL -> {
val call = baseAttachment.call ?: continue
attachments += call.toDomain()
}
AttachmentType.GROUP_CALL_IN_PROGRESS -> {
val groupCall = baseAttachment.groupCall ?: continue
attachments += groupCall.toDomain()
}
AttachmentType.CURATOR -> {
val curator = baseAttachment.curator ?: continue
attachments += curator.toDomain()
}
AttachmentType.EVENT -> {
val event = baseAttachment.event ?: continue
attachments += event.toDomain()
}
AttachmentType.STORY -> {
val story = baseAttachment.story ?: continue
attachments += story.toDomain()
}
AttachmentType.WIDGET -> {
val widget = baseAttachment.widget ?: continue
attachments += widget.toDomain()
}
AttachmentType.ARTIST -> {
val artist = baseAttachment.artist ?: continue
attachments += artist.toDomain()
val audios = baseAttachment.audios ?: continue
audios.map(VkAudioData::toDomain).let(attachments::addAll)
}
AttachmentType.AUDIO_PLAYLIST -> {
val audioPlaylist = baseAttachment.audioPlaylist ?: continue
attachments += audioPlaylist.toDomain()
}
AttachmentType.PODCAST -> {
val podcast = baseAttachment.podcast ?: continue
attachments += podcast.toDomain()
}
}
}
return attachments
}
@@ -28,6 +28,7 @@ data class VkPinnedMessageData(
fun mapToDomain(): VkMessage = VkMessage(
id = id ?: -1,
conversationMessageId = conversationMessageId,
text = text.ifBlank { null },
isOut = out == true,
peerId = peerId ?: -1,
@@ -0,0 +1,10 @@
package com.meloda.app.fast.model.api.domain
data class VkAttachmentHistoryMessage(
val messageId: Int,
val conversationMessageId: Int,
val date: Int,
val fromId: Int,
val position: Int,
val attachment: VkAttachment
)
@@ -4,6 +4,7 @@ import com.meloda.app.fast.model.database.VkMessageEntity
data class VkMessage(
val id: Int,
val conversationMessageId: Int,
val text: String?,
val isOut: Boolean,
val peerId: Int,
@@ -78,6 +79,7 @@ data class VkMessage(
fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity(
id = id,
conversationMessageId = conversationMessageId,
text = text,
isOut = isOut,
peerId = peerId,
@@ -0,0 +1,7 @@
package com.meloda.app.fast.model.api.domain
import com.meloda.app.fast.model.api.data.AttachmentType
data object VkUnknownAttachment : VkAttachment {
override val type: AttachmentType = AttachmentType.UNKNOWN
}
@@ -243,3 +243,27 @@ data class MessagesRemoveChatUserRequest(
"member_id" to memberId.toString()
)
}
data class MessagesGetHistoryAttachmentsRequest(
val peerId: Int,
val extended: Boolean?,
val count: Int?,
val offset: Int?,
val preserveOrder: Boolean?,
val attachmentTypes: List<String>,
val conversationMessageId: Int,
val fields: String?
) {
val map = mutableMapOf(
"peer_id" to peerId.toString(),
"attachment_types" to attachmentTypes.joinToString(","),
"cmid" to conversationMessageId.toString()
).apply {
extended?.let { this["extended"] = it.toString() }
count?.let { this["count"] = it.toString() }
offset?.let { this["offset"] = it.toString() }
preserveOrder?.let { this["preserve_order"] = it.toString() }
fields?.let { this["fields"] = it }
}
}
@@ -1,11 +1,13 @@
package com.meloda.app.fast.model.api.responses
import com.meloda.app.fast.model.api.data.VkAttachmentHistoryMessageData
import com.meloda.app.fast.model.api.data.VkChatMemberData
import com.meloda.app.fast.model.api.data.VkContactData
import com.meloda.app.fast.model.api.data.VkConversationData
import com.meloda.app.fast.model.api.data.VkGroupData
import com.meloda.app.fast.model.api.data.VkMessageData
import com.meloda.app.fast.model.api.data.VkUserData
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
@@ -33,3 +35,12 @@ data class MessagesGetConversationMembersResponse(
val profiles: List<VkUserData>?,
val groups: List<VkGroupData>?
)
@JsonClass(generateAdapter = true)
data class MessagesGetHistoryAttachmentsResponse(
@Json(name = "items") val items: List<VkAttachmentHistoryMessageData>,
@Json(name = "next_from") val nextFrom: String?,
@Json(name = "profiles") val profiles: List<VkUserData>?,
@Json(name = "groups") val groups: List<VkGroupData>?,
@Json(name = "contacts") val contacts: List<VkContactData>?
)
@@ -8,6 +8,7 @@ import com.meloda.app.fast.model.api.domain.VkMessage
@Entity(tableName = "messages")
data class VkMessageEntity(
@PrimaryKey val id: Int,
val conversationMessageId: Int,
val text: String?,
val isOut: Boolean,
val peerId: Int,
@@ -29,6 +30,7 @@ data class VkMessageEntity(
fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage(
id = id,
conversationMessageId = conversationMessageId,
text = text,
isOut = isOut,
peerId = peerId,
@@ -2,6 +2,7 @@ package com.meloda.app.fast.network.service.messages
import com.meloda.app.fast.model.api.data.VkLongPollData
import com.meloda.app.fast.model.api.responses.MessagesGetByIdResponse
import com.meloda.app.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse
import com.meloda.app.fast.model.api.responses.MessagesGetHistoryResponse
import com.meloda.app.fast.network.ApiResponse
import com.meloda.app.fast.network.RestApiError
@@ -42,6 +43,12 @@ interface MessagesService {
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(MessagesUrls.GET_HISTORY_ATTACHMENTS)
suspend fun getHistoryAttachments(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<MessagesGetHistoryAttachmentsResponse>, RestApiError>
// @FormUrlEncoded
// @POST(MessagesUrls.MarkAsImportant)
// suspend fun markAsImportant(
@@ -18,4 +18,5 @@ object MessagesUrls {
const val GET_CHAT = "${AppConstants.URL_API}/messages.getChat"
const val GET_CONVERSATIONS_MEMBERS = "${AppConstants.URL_API}/messages.getConversationMembers"
const val REMOVE_CHAT_USER = "${AppConstants.URL_API}/messages.removeChatUser"
const val GET_HISTORY_ATTACHMENTS = "${AppConstants.URL_API}/messages.getHistoryAttachments"
}
+1
View File
@@ -27,6 +27,7 @@ android {
}
kotlinOptions {
jvmTarget = Configs.java.toString()
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers")
}
buildFeatures {
compose = true
@@ -1,91 +0,0 @@
package com.meloda.app.fast.chatmaterials
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalHazeMaterialsApi::class
)
@Composable
fun ChatMaterialsScreen(
onBack: () -> Unit
) {
val hazeState = remember { HazeState() }
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "Chat Materials")
},
colors = TopAppBarDefaults.largeTopAppBarColors(Color.Transparent),
modifier = Modifier
.hazeChild(
state = hazeState,
style = HazeMaterials.ultraThin()
)
.fillMaxWidth(),
navigationIcon = {
IconButton(
onClick = onBack
) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
}
)
}
) {
LazyVerticalGrid(
columns = GridCells.Adaptive(200.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
.haze(
state = hazeState,
style = HazeMaterials.ultraThin()
)
) {
items(100) { index ->
val link = "https://random.imagecdn.app/500/150"
AsyncImage(
model = link,
contentDescription = "Image",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
)
}
}
}
}
@@ -0,0 +1,130 @@
package com.meloda.app.fast.chatmaterials
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.meloda.app.fast.chatmaterials.model.ChatMaterialsScreenState
import com.meloda.app.fast.chatmaterials.navigation.ChatMaterials
import com.meloda.app.fast.chatmaterials.util.asPresentation
import com.meloda.app.fast.common.extensions.listenValue
import com.meloda.app.fast.common.extensions.setValue
import com.meloda.app.fast.data.api.messages.MessagesUseCase
import com.meloda.app.fast.data.processState
import com.meloda.app.fast.model.BaseError
import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
interface ChatMaterialsViewModel {
val screenState: StateFlow<ChatMaterialsScreenState>
val baseError: StateFlow<BaseError?>
val imagesToPreload: StateFlow<List<String>>
val currentOffset: StateFlow<Int>
val canPaginate: StateFlow<Boolean>
fun onMetPaginationCondition()
fun onRefresh()
fun onErrorConsumed()
fun onTypeChanged(newType: String)
}
class ChatMaterialsViewModelImpl(
private val messagesUseCase: MessagesUseCase,
savedStateHandle: SavedStateHandle
) : ViewModel(), ChatMaterialsViewModel {
override val screenState = MutableStateFlow(ChatMaterialsScreenState.EMPTY)
override val baseError = MutableStateFlow<BaseError?>(null)
override val imagesToPreload = MutableStateFlow<List<String>>(emptyList())
override val currentOffset = MutableStateFlow(0)
override val canPaginate = MutableStateFlow(false)
init {
val arguments = ChatMaterials.from(savedStateHandle)
screenState.setValue { old ->
old.copy(
peerId = arguments.peerId,
conversationMessageId = arguments.conversationMessageId
)
}
loadChatMaterials()
}
override fun onMetPaginationCondition() {
currentOffset.update { screenState.value.materials.size }
loadChatMaterials()
}
override fun onRefresh() {
loadChatMaterials(offset = 0)
}
override fun onErrorConsumed() {
baseError.setValue { null }
}
override fun onTypeChanged(newType: String) {
screenState.setValue { old -> old.copy(attachmentType = newType) }
loadChatMaterials(0)
}
private fun loadChatMaterials(
offset: Int = currentOffset.value
) {
messagesUseCase.getHistoryAttachments(
peerId = screenState.value.peerId,
count = LOAD_COUNT,
offset = offset,
attachmentTypes = listOf(screenState.value.attachmentType),
conversationMessageId = screenState.value.conversationMessageId
).listenValue { state ->
state.processState(
error = { error ->
},
success = { response ->
val itemsCountSufficient = response.size == LOAD_COUNT
canPaginate.setValue { itemsCountSufficient }
val paginationExhausted = !itemsCountSufficient &&
screenState.value.materials.size >= LOAD_COUNT
val loadedMaterials = response.map(VkAttachmentHistoryMessage::asPresentation)
val newState = screenState.value.copy(
isPaginationExhausted = paginationExhausted
)
if (offset == 0) {
screenState.setValue {
newState.copy(materials = loadedMaterials)
}
} else {
screenState.setValue {
newState.copy(
materials = newState.materials.plus(loadedMaterials)
)
}
}
}
)
screenState.setValue { old ->
old.copy(
isLoading = offset == 0 && state.isLoading(),
isPaginating = offset > 0 && state.isLoading()
)
}
}
}
companion object {
const val LOAD_COUNT = 100
}
}
@@ -0,0 +1,9 @@
package com.meloda.app.fast.chatmaterials.di
import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModelImpl
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.dsl.module
val chatMaterialsModule = module {
viewModelOf(::ChatMaterialsViewModelImpl)
}
@@ -0,0 +1,24 @@
package com.meloda.app.fast.chatmaterials.model
data class ChatMaterialsScreenState(
val isLoading: Boolean,
val materials: List<UiChatMaterial>,
val attachmentType: String,
val isPaginating: Boolean,
val isPaginationExhausted: Boolean,
val peerId: Int,
val conversationMessageId: Int
) {
companion object {
val EMPTY: ChatMaterialsScreenState = ChatMaterialsScreenState(
isLoading = true,
materials = emptyList(),
attachmentType = "photo",
isPaginating = false,
isPaginationExhausted = false,
peerId = -1,
conversationMessageId = -1
)
}
}
@@ -0,0 +1,28 @@
package com.meloda.app.fast.chatmaterials.model
sealed class UiChatMaterial {
data class Photo(
val previewUrl: String
) : UiChatMaterial()
data class Video(
val previewUrl: String
) : UiChatMaterial()
data class Audio(
val previewUrl: String?,
val title: String,
val artist: String,
val duration: String
) : UiChatMaterial()
data class File(
val title: String
) : UiChatMaterial()
data class Link(
val title: String,
val previewUrl: String?
) : UiChatMaterial()
}
@@ -1,13 +1,23 @@
package com.meloda.app.fast.chatmaterials.navigation
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.meloda.app.fast.chatmaterials.ChatMaterialsScreen
import androidx.navigation.toRoute
import com.meloda.app.fast.chatmaterials.presentation.ChatMaterialsScreen
import kotlinx.serialization.Serializable
@Serializable
data class ChatMaterials(val a: String)
data class ChatMaterials(
val peerId: Int,
val conversationMessageId: Int
) {
companion object {
fun from(savedStateHandle: SavedStateHandle) =
savedStateHandle.toRoute<ChatMaterials>()
}
}
fun NavGraphBuilder.chatMaterialsRoute(
onBack: () -> Unit
@@ -19,6 +29,11 @@ fun NavGraphBuilder.chatMaterialsRoute(
}
}
fun NavController.navigateToChatMaterials() {
this.navigate(ChatMaterials(""))
fun NavController.navigateToChatMaterials(peerId: Int, conversationMessageId: Int) {
this.navigate(
ChatMaterials(
peerId = peerId,
conversationMessageId = conversationMessageId
)
)
}
@@ -0,0 +1,68 @@
package com.meloda.app.fast.chatmaterials.presentation
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import coil.ImageLoader
import coil.compose.AsyncImage
import com.meloda.app.fast.chatmaterials.model.UiChatMaterial
@Composable
fun ChatMaterialItem(
item: UiChatMaterial,
imageLoader: ImageLoader
) {
when (item) {
is UiChatMaterial.Photo -> {
AsyncImage(
model = item.previewUrl,
contentDescription = null,
imageLoader = imageLoader,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
)
}
is UiChatMaterial.Video -> {
AsyncImage(
model = item.previewUrl,
contentDescription = null,
imageLoader = imageLoader,
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.aspectRatio(1f)
)
}
is UiChatMaterial.Audio -> {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = item.title,
style = MaterialTheme.typography.bodyLarge
)
Text(text = item.artist)
}
Text(text = item.duration)
}
}
is UiChatMaterial.File -> {}
is UiChatMaterial.Link -> {}
}
}
@@ -0,0 +1,352 @@
package com.meloda.app.fast.chatmaterials.presentation
import android.annotation.SuppressLint
import android.util.Log
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.pulltorefresh.PullToRefreshContainer
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.imageLoader
import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModel
import com.meloda.app.fast.chatmaterials.ChatMaterialsViewModelImpl
import com.meloda.app.fast.designsystem.LocalTheme
import com.meloda.app.fast.designsystem.R
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import org.koin.androidx.compose.koinViewModel
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@OptIn(
ExperimentalMaterial3Api::class,
ExperimentalHazeMaterialsApi::class
)
@Composable
fun ChatMaterialsScreen(
onBack: () -> Unit,
viewModel: ChatMaterialsViewModel = koinViewModel<ChatMaterialsViewModelImpl>()
) {
val currentTheme = LocalTheme.current
val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val attachments = screenState.materials
val imageLoader = LocalContext.current.imageLoader
var moreClearBlur by rememberSaveable {
mutableStateOf(false)
}
val hazeState = remember { HazeState() }
val hazeStyle = if (moreClearBlur) HazeMaterials.ultraThin() else HazeMaterials.regular()
var dropDownMenuExpanded by remember {
mutableStateOf(false)
}
var checkedTypeIndex by rememberSaveable {
mutableIntStateOf(0)
}
LaunchedEffect(checkedTypeIndex) {
viewModel.onTypeChanged(
when (checkedTypeIndex) {
0 -> "photo"
1 -> "video"
2 -> "audio"
3 -> "doc"
4 -> "link"
else -> ""
}
)
}
val titles = listOf("Photos", "Videos", "Audios", "Files", "Links")
val listState = rememberLazyListState()
val gridState = rememberLazyGridState()
val canScrollBackward = when (checkedTypeIndex) {
in 0..1 -> gridState.canScrollBackward
else -> listState.canScrollBackward
}
Log.d("ChatMaterialsScreen", "ChatMaterialsScreen: canScrollBackward: $canScrollBackward")
val toolbarColorAlpha by animateFloatAsState(
targetValue = if (!canScrollBackward) 1f else 0f,
label = "toolbarColorAlpha",
animationSpec = tween(durationMillis = 50)
)
val toolbarContainerColor by animateColorAsState(
targetValue =
if (currentTheme.usingBlur || !canScrollBackward)
MaterialTheme.colorScheme.surface
else
MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
label = "toolbarColorAlpha",
animationSpec = tween(durationMillis = 50)
)
val pullToRefreshAlpha by animateFloatAsState(
targetValue = if (!canScrollBackward) 1f else 0f,
label = "pullToRefreshAlpha",
animationSpec = tween(durationMillis = 50)
)
val pullToRefreshState = rememberPullToRefreshState()
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = "Chat Materials")
},
modifier = Modifier
.then(
if (currentTheme.usingBlur) {
Modifier.hazeChild(
state = hazeState,
style = hazeStyle
)
} else Modifier
)
.fillMaxWidth(),
colors = TopAppBarDefaults.topAppBarColors(
containerColor = toolbarContainerColor.copy(
alpha = if (currentTheme.usingBlur) toolbarColorAlpha else 1f
)
),
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
contentDescription = null
)
}
},
actions = {
IconButton(
onClick = {
dropDownMenuExpanded = true
}
) {
Icon(
imageVector = Icons.Outlined.MoreVert,
contentDescription = "Options button"
)
}
DropdownMenu(
modifier = Modifier.defaultMinSize(minWidth = 140.dp),
expanded = dropDownMenuExpanded,
onDismissRequest = {
dropDownMenuExpanded = false
},
offset = DpOffset(x = (-4).dp, y = (-60).dp)
) {
DropdownMenuItem(
onClick = {
viewModel.onRefresh()
dropDownMenuExpanded = false
},
text = {
Text(text = stringResource(id = R.string.action_refresh))
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Refresh,
contentDescription = null
)
}
)
if (currentTheme.usingBlur) {
DropdownMenuItem(
text = {
Text(text = if (moreClearBlur) "Default blur" else "Clearer blur")
},
onClick = {
moreClearBlur = !moreClearBlur
dropDownMenuExpanded = false
}
)
}
HorizontalDivider()
titles.forEachIndexed { index, title ->
DropdownMenuItem(
leadingIcon = {
RadioButton(
selected = checkedTypeIndex == index,
onClick = null
)
},
text = {
Text(text = title)
},
onClick = {
checkedTypeIndex = index
dropDownMenuExpanded = false
}
)
}
}
}
)
}
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(start = padding.calculateStartPadding(LayoutDirection.Ltr))
.padding(end = padding.calculateEndPadding(LayoutDirection.Ltr))
.nestedScroll(pullToRefreshState.nestedScrollConnection)
) {
if (checkedTypeIndex in listOf(0, 1)) {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
state = gridState,
horizontalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.then(
if (currentTheme.usingBlur) {
Modifier.haze(
state = hazeState,
style = hazeStyle
)
} else {
Modifier
}
)
.fillMaxSize()
) {
repeat(3) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
}
items(attachments) { item ->
ChatMaterialItem(
item = item,
imageLoader = imageLoader
)
}
repeat(3) {
item {
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
}
}
}
} else {
LazyColumn(
state = listState,
modifier = Modifier
.then(
if (currentTheme.usingBlur) {
Modifier.haze(
state = hazeState,
style = hazeStyle
)
} else {
Modifier
}
)
.fillMaxSize()
) {
item {
Spacer(modifier = Modifier.height(padding.calculateTopPadding()))
}
items(attachments) { item ->
ChatMaterialItem(
item = item,
imageLoader = imageLoader
)
}
item {
Spacer(modifier = Modifier.height(padding.calculateBottomPadding()))
}
}
}
if (pullToRefreshState.isRefreshing) {
LaunchedEffect(true) {
viewModel.onRefresh()
}
}
LaunchedEffect(screenState.isLoading) {
if (!screenState.isLoading) {
pullToRefreshState.endRefresh()
}
}
PullToRefreshContainer(
state = pullToRefreshState,
modifier = Modifier
.alpha(pullToRefreshAlpha)
.align(Alignment.TopCenter)
.padding(top = padding.calculateTopPadding()),
contentColor = MaterialTheme.colorScheme.primary
)
}
}
}
@@ -0,0 +1,59 @@
package com.meloda.app.fast.chatmaterials.util
import com.meloda.app.fast.chatmaterials.model.UiChatMaterial
import com.meloda.app.fast.model.api.data.AttachmentType
import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage
import com.meloda.app.fast.model.api.domain.VkAudioDomain
import com.meloda.app.fast.model.api.domain.VkFileDomain
import com.meloda.app.fast.model.api.domain.VkLinkDomain
import com.meloda.app.fast.model.api.domain.VkPhotoDomain
import com.meloda.app.fast.model.api.domain.VkVideoDomain
import java.text.SimpleDateFormat
import java.util.Locale
fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial =
when (val type = this.attachment.type) {
AttachmentType.PHOTO -> {
val attachment = this.attachment as VkPhotoDomain
UiChatMaterial.Photo(
previewUrl = attachment.getSizeOrSmaller(VkPhotoDomain.SIZE_TYPE_1080_1024)?.url.orEmpty()
)
}
AttachmentType.VIDEO -> {
val attachment = this.attachment as VkVideoDomain
UiChatMaterial.Video(
previewUrl = attachment.images.firstOrNull()?.url.orEmpty()
)
}
AttachmentType.AUDIO -> {
val attachment = this.attachment as VkAudioDomain
UiChatMaterial.Audio(
previewUrl = null,
title = attachment.title,
artist = attachment.artist,
duration = SimpleDateFormat(
"mm:ss",
Locale.getDefault()
).format(attachment.duration)
)
}
AttachmentType.FILE -> {
val attachment = this.attachment as VkFileDomain
UiChatMaterial.File(
title = attachment.title
)
}
AttachmentType.LINK -> {
val attachment = this.attachment as VkLinkDomain
UiChatMaterial.Link(
title = attachment.title ?: attachment.url,
previewUrl = attachment.photo?.getMaxSize()?.url
)
}
else -> throw IllegalArgumentException("Unsupported type: $type")
}
@@ -27,6 +27,7 @@ import com.meloda.app.fast.common.UserConfig
import com.meloda.app.fast.conversations.model.ConversationOption
import com.meloda.app.fast.conversations.model.ConversationsScreenState
import com.meloda.app.fast.conversations.model.UiConversation
import com.meloda.app.fast.designsystem.LocalBottomPadding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -45,6 +46,8 @@ fun ConversationsListComposable(
val conversations = screenState.conversations
val bottomPadding = LocalBottomPadding.current
LazyColumn(
modifier = modifier,
state = state
@@ -105,5 +108,9 @@ fun ConversationsListComposable(
}
}
}
item {
Spacer(modifier = Modifier.height(bottomPadding))
}
}
}
@@ -5,16 +5,20 @@ import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
@@ -61,6 +65,7 @@ import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.imageLoader
@@ -70,6 +75,8 @@ import com.meloda.app.fast.conversations.ConversationsViewModel
import com.meloda.app.fast.conversations.ConversationsViewModelImpl
import com.meloda.app.fast.conversations.model.ConversationsScreenState
import com.meloda.app.fast.conversations.model.UiConversation
import com.meloda.app.fast.designsystem.LocalBottomPadding
import com.meloda.app.fast.designsystem.LocalHazeState
import com.meloda.app.fast.designsystem.LocalTheme
import com.meloda.app.fast.designsystem.MaterialDialog
import com.meloda.app.fast.designsystem.components.FullScreenLoader
@@ -142,7 +149,8 @@ fun ConversationsScreen(
}
}
val hazeState = remember { HazeState() }
// val hazeState = remember { HazeState() }
val hazeState = LocalHazeState.current
var dropDownMenuExpanded by remember {
mutableStateOf(false)
@@ -252,11 +260,12 @@ fun ConversationsScreen(
val scope = rememberCoroutineScope()
val rotation = remember { Animatable(0f) }
Column {
AnimatedVisibility(
visible = isListScrollingUp,
modifier = Modifier.navigationBarsPadding(),
enter = slideIn { IntOffset(0, 400) },
exit = slideOut { IntOffset(0, 400) }
enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
) {
FloatingActionButton(
onClick = {
@@ -285,6 +294,9 @@ fun ConversationsScreen(
)
}
}
Spacer(modifier = Modifier.height(LocalBottomPadding.current))
}
}
) { padding ->
when {
@@ -77,7 +77,7 @@ class FriendsViewModelImpl(
}
private fun loadFriends(offset: Int = currentOffset.value) {
friendsUseCase.getAllFriends(count = 30, offset = offset).listenValue { state ->
friendsUseCase.getAllFriends(count = LOAD_COUNT, offset = offset).listenValue { state ->
state.processState(
error = { error ->
when (error) {
@@ -103,11 +103,11 @@ class FriendsViewModelImpl(
},
success = { info ->
val response = info.friends
val itemsCountSufficient = response.size == 30
val itemsCountSufficient = response.size == LOAD_COUNT
canPaginate.setValue { itemsCountSufficient }
val paginationExhausted = !itemsCountSufficient &&
screenState.value.friends.size >= 30
screenState.value.friends.size >= LOAD_COUNT
imagesToPreload.setValue {
response.mapNotNull(VkUser::photo100)
@@ -172,4 +172,8 @@ class FriendsViewModelImpl(
}
uiOnlineFriends.setValue { onlineUiFriends }
}
companion object {
const val LOAD_COUNT = 30
}
}
@@ -354,6 +354,7 @@ class MessagesHistoryViewModelImpl(
val newMessage = VkMessage(
id = -1 - sendingMessages.size,
conversationMessageId = -1,
text = lastMessageText,
isOut = true,
peerId = screenState.value.conversationId,
@@ -1,27 +1,28 @@
package com.meloda.app.fast.messageshistory.domain
import com.meloda.app.fast.data.State
import com.meloda.app.fast.data.api.messages.MessagesHistoryDomain
import com.meloda.app.fast.data.api.messages.MessagesHistoryInfo
import com.meloda.app.fast.data.api.messages.MessagesRepository
import com.meloda.app.fast.data.api.messages.MessagesUseCase
import com.meloda.app.fast.data.mapToState
import com.meloda.app.fast.model.api.domain.VkAttachment
import com.meloda.app.fast.model.api.domain.VkAttachmentHistoryMessage
import com.meloda.app.fast.model.api.domain.VkMessage
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
class MessagesUseCaseImpl(
private val messagesRepository: MessagesRepository
private val repository: MessagesRepository
) : MessagesUseCase {
override fun getMessagesHistory(
conversationId: Int,
count: Int?,
offset: Int?
): Flow<State<MessagesHistoryDomain>> = flow {
): Flow<State<MessagesHistoryInfo>> = flow {
emit(State.Loading)
val newState = messagesRepository.getMessagesHistory(
val newState = repository.getHistory(
conversationId = conversationId,
offset = offset,
count = count
@@ -31,60 +32,20 @@ class MessagesUseCaseImpl(
}
override fun getById(
messageId: Int,
extended: Boolean?,
fields: String?
): Flow<State<VkMessage?>> = flow {
emit(State.Loading)
val newState = messagesRepository.getMessageById(
messagesIds = listOf(messageId),
extended = extended,
fields = fields
).mapToState()
emit(newState)
}
override fun getByIds(
messageIds: List<Int>,
extended: Boolean?,
fields: String?
): Flow<State<List<VkMessage>>> = flow {}
// flow {
// emit(State.Loading)
//
// val newState = messagesRepository.getById(
// params = MessagesGetByIdRequest(
// messagesIds = messageIds,
// extended = extended,
// fields = fields
// )
// ).fold(
// onSuccess = { response ->
// val messages = response.items
// val usersMap =
// VkUsersMap.forUsers(response.profiles.orEmpty().map(VkUserData::mapToDomain))
// val groupsMap =
// VkGroupsMap.forGroups(response.groups.orEmpty().map(VkGroupData::mapToDomain))
//
// com.meloda.app.fast.network.State.Success(
// messages.map { message ->
// message.mapToDomain(
// user = usersMap.messageUser(message),
// group = groupsMap.messageGroup(message),
// actionUser = usersMap.messageActionUser(message),
// actionGroup = groupsMap.messageActionGroup(message)
// )
// }
// )
// },
// onNetworkFailure = { com.meloda.app.fast.network.State.Error.ConnectionError },
// onUnknownFailure = { com.meloda.app.fast.network.State.UNKNOWN_ERROR },
// onHttpFailure = { result -> result.error.toStateApiError() },
// onApiFailure = { result -> result.error.toStateApiError() }
// )
// emit(newState)
// }
): Flow<State<List<VkMessage>>> = flow {
emit(State.Loading)
val newState = repository.getById(
messagesIds = messageIds,
extended = extended,
fields = fields
).mapToState()
emit(newState)
}
override fun sendMessage(
peerId: Int,
@@ -95,7 +56,7 @@ class MessagesUseCaseImpl(
): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = messagesRepository.send(
val newState = repository.send(
peerId = peerId,
randomId = randomId,
message = message,
@@ -112,7 +73,7 @@ class MessagesUseCaseImpl(
): Flow<State<Int>> = flow {
emit(State.Loading)
val newState = messagesRepository.markAsRead(
val newState = repository.markAsRead(
peerId = peerId,
startMessageId = startMessageId
).mapToState()
@@ -120,11 +81,31 @@ class MessagesUseCaseImpl(
emit(newState)
}
override fun getHistoryAttachments(
peerId: Int,
count: Int?,
offset: Int?,
attachmentTypes: List<String>,
conversationMessageId: Int
): Flow<State<List<VkAttachmentHistoryMessage>>> = flow {
emit(State.Loading)
val newState = repository.getHistoryAttachments(
peerId = peerId,
count = count,
offset = offset,
attachmentTypes = attachmentTypes,
conversationMessageId = conversationMessageId
).mapToState()
emit(newState)
}
override suspend fun storeMessage(message: VkMessage) {
messagesRepository.storeMessages(listOf(message))
repository.storeMessages(listOf(message))
}
override suspend fun storeMessages(messages: List<VkMessage>) {
messagesRepository.storeMessages(messages)
repository.storeMessages(messages)
}
}
@@ -4,6 +4,7 @@ import com.meloda.app.fast.common.UiImage
data class UiMessage(
val id: Int,
val conversationMessageId: Int,
val text: String?,
val isOut: Boolean,
val fromId: Int,
@@ -40,7 +40,7 @@ val MessagesHistoryNavType = object : NavType<MessagesHistoryArguments>(isNullab
fun NavGraphBuilder.messagesHistoryRoute(
onError: (BaseError) -> Unit,
onBack: () -> Unit,
onNavigateToChatAttachments: () -> Unit
onNavigateToChatAttachments: (peerId: Int, conversationMessageId: Int) -> Unit
) {
composable<MessagesHistory>(
typeMap = mapOf(typeOf<MessagesHistoryArguments>() to MessagesHistoryNavType)
@@ -90,7 +90,7 @@ import com.meloda.app.fast.designsystem.R as UiR
fun MessagesHistoryScreen(
onError: (BaseError) -> Unit,
onBack: () -> Unit,
onNavigateToChatMaterials: () -> Unit,
onNavigateToChatMaterials: (peerId: Int, conversationMessageId: Int) -> Unit,
viewModel: MessagesHistoryViewModel = koinViewModel<MessagesHistoryViewModelImpl>()
) {
val view = LocalView.current
@@ -215,7 +215,12 @@ fun MessagesHistoryScreen(
DropdownMenuItem(
onClick = {
dropDownMenuExpanded = false
onNavigateToChatMaterials()
// TODO: 11/07/2024, Danil Nikolaev: to VM
onNavigateToChatMaterials(
screenState.conversationId,
screenState.messages.first().conversationMessageId
)
},
text = {
Text(text = "Materials")
@@ -91,6 +91,7 @@ fun VkMessage.asPresentation(
nextMessage: VkMessage?
): UiMessage = UiMessage(
id = id,
conversationMessageId = conversationMessageId,
text = text,
isOut = isOut,
fromId = fromId,
+2 -3
View File
@@ -2,7 +2,7 @@
agp = "8.5.0"
converterMoshi = "2.11.0"
eithernet = "1.9.0"
haze = "0.7.2"
haze = "0.7.3"
kotlin = "2.0.0"
ksp = "2.0.0-1.0.22"
@@ -26,7 +26,7 @@ nanokt = "1.2.0"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
appcompat = "1.7.0"
androidx-navigation = "2.8.0-beta04"
androidx-navigation = "2.8.0-beta05"
serialization = "1.7.1"
[libraries]
@@ -47,7 +47,6 @@ compose-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable
compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
eithernet = { module = "com.slack.eithernet:eithernet", version.ref = "eithernet" }
eithernet-retrofit-integration = { module = "com.slack.eithernet:eithernet-integration-retrofit", version.ref = "eithernet" }
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
junit = { module = "junit:junit", version.ref = "junit" }