forked from melod1n/fast-messenger
[wip] chat materials; some experiments with local composition and blur
This commit is contained in:
@@ -9,6 +9,7 @@ import androidx.compose.animation.slideOut
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
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.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarDefaults
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
@@ -36,9 +41,16 @@ import androidx.navigation.compose.navigation
|
|||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.meloda.app.fast.conversations.navigation.Conversations
|
import com.meloda.app.fast.conversations.navigation.Conversations
|
||||||
import com.meloda.app.fast.conversations.navigation.conversationsRoute
|
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.Friends
|
||||||
import com.meloda.app.fast.friends.navigation.friendsRoute
|
import com.meloda.app.fast.friends.navigation.friendsRoute
|
||||||
import com.meloda.app.fast.model.BaseError
|
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 kotlinx.serialization.Serializable
|
||||||
import com.meloda.app.fast.designsystem.R as UiR
|
import com.meloda.app.fast.designsystem.R as UiR
|
||||||
|
|
||||||
@@ -58,7 +70,7 @@ data class BottomNavigationItem(
|
|||||||
val route: Any,
|
val route: Any,
|
||||||
)
|
)
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class)
|
||||||
fun NavGraphBuilder.mainScreen(
|
fun NavGraphBuilder.mainScreen(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onNavigateToSettings: () -> Unit,
|
onNavigateToSettings: () -> Unit,
|
||||||
@@ -87,6 +99,8 @@ fun NavGraphBuilder.mainScreen(
|
|||||||
val routes = items.map(BottomNavigationItem::route)
|
val routes = items.map(BottomNavigationItem::route)
|
||||||
|
|
||||||
composable<Main> {
|
composable<Main> {
|
||||||
|
val currentTheme = LocalTheme.current
|
||||||
|
val hazeState = remember { HazeState() }
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
|
|
||||||
var selectedItemIndex by rememberSaveable {
|
var selectedItemIndex by rememberSaveable {
|
||||||
@@ -104,7 +118,21 @@ fun NavGraphBuilder.mainScreen(
|
|||||||
enter = slideIn { IntOffset(0, 400) },
|
enter = slideIn { IntOffset(0, 400) },
|
||||||
exit = slideOut { 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 ->
|
items.forEachIndexed { index, item ->
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
selected = selectedItemIndex == index,
|
selected = selectedItemIndex == index,
|
||||||
@@ -139,7 +167,11 @@ fun NavGraphBuilder.mainScreen(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.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(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
@@ -194,3 +226,4 @@ fun NavGraphBuilder.mainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.os.PowerManager
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.meloda.app.fast.MainViewModelImpl
|
import com.meloda.app.fast.MainViewModelImpl
|
||||||
import com.meloda.app.fast.auth.authModule
|
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.conversations.di.conversationsModule
|
||||||
import com.meloda.app.fast.data.di.dataModule
|
import com.meloda.app.fast.data.di.dataModule
|
||||||
import com.meloda.app.fast.friends.di.friendsModule
|
import com.meloda.app.fast.friends.di.friendsModule
|
||||||
@@ -32,7 +33,8 @@ val applicationModule = module {
|
|||||||
languagePickerModule,
|
languagePickerModule,
|
||||||
longPollModule,
|
longPollModule,
|
||||||
friendsModule,
|
friendsModule,
|
||||||
profileModule
|
profileModule,
|
||||||
|
chatMaterialsModule
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors
|
// TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors
|
||||||
|
|||||||
@@ -231,13 +231,13 @@ class LongPollUpdatesParser(
|
|||||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun <T : LongPollEvent> loadNormalMessage(
|
private suspend inline fun <reified T : LongPollEvent> loadNormalMessage(
|
||||||
eventType: ApiEvent,
|
eventType: ApiEvent,
|
||||||
messageId: Int
|
messageId: Int
|
||||||
): T? = suspendCoroutine {
|
): T? = suspendCoroutine { continuation ->
|
||||||
coroutineScope.launch(Dispatchers.IO) {
|
coroutineScope.launch(Dispatchers.IO) {
|
||||||
messagesUseCase.getById(
|
messagesUseCase.getById(
|
||||||
messageId = messageId,
|
messageIds = listOf(messageId),
|
||||||
extended = true,
|
extended = true,
|
||||||
fields = VkConstants.ALL_FIELDS
|
fields = VkConstants.ALL_FIELDS
|
||||||
).listenValue(this) { state ->
|
).listenValue(this) { state ->
|
||||||
@@ -245,8 +245,12 @@ class LongPollUpdatesParser(
|
|||||||
error = { error ->
|
error = { error ->
|
||||||
Log.e("LongPollUpdatesParser", "loadNormalMessage: error: $error")
|
Log.e("LongPollUpdatesParser", "loadNormalMessage: error: $error")
|
||||||
},
|
},
|
||||||
success = { response ->
|
success = { messages ->
|
||||||
response?.let { message ->
|
val message = messages.singleOrNull() ?: run {
|
||||||
|
continuation.resume(null)
|
||||||
|
return@listenValue
|
||||||
|
}
|
||||||
|
|
||||||
VkMemoryCache[message.id] = message
|
VkMemoryCache[message.id] = message
|
||||||
messagesUseCase.storeMessage(message)
|
messagesUseCase.storeMessage(message)
|
||||||
|
|
||||||
@@ -254,11 +258,13 @@ class LongPollUpdatesParser(
|
|||||||
ApiEvent.MESSAGE_NEW -> LongPollEvent.VkMessageNewEvent(message)
|
ApiEvent.MESSAGE_NEW -> LongPollEvent.VkMessageNewEvent(message)
|
||||||
ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(message)
|
ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(message)
|
||||||
|
|
||||||
else -> null
|
else -> {
|
||||||
|
continuation.resume(null)
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resumeValue?.let { value -> it.resume(value as T) }
|
resumeValue?.let { value -> continuation.resume(value as T) }
|
||||||
} ?: it.resume(null)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
)
|
||||||
-16
@@ -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>)
|
|
||||||
}
|
|
||||||
-24
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-36
@@ -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?
|
|
||||||
}
|
|
||||||
-164
@@ -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>
|
|
||||||
)
|
|
||||||
+12
-10
@@ -1,24 +1,24 @@
|
|||||||
package com.meloda.app.fast.data.api.messages
|
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.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.VkMessage
|
||||||
import com.meloda.app.fast.network.RestApiErrorDomain
|
import com.meloda.app.fast.network.RestApiErrorDomain
|
||||||
import com.slack.eithernet.ApiResult
|
import com.slack.eithernet.ApiResult
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
|
|
||||||
interface MessagesRepository {
|
interface MessagesRepository {
|
||||||
|
|
||||||
suspend fun getMessagesHistory(
|
suspend fun getHistory(
|
||||||
conversationId: Int,
|
conversationId: Int,
|
||||||
offset: Int?,
|
offset: Int?,
|
||||||
count: Int?
|
count: Int?
|
||||||
): ApiResult<MessagesHistoryDomain, RestApiErrorDomain>
|
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain>
|
||||||
|
|
||||||
suspend fun getMessageById(
|
suspend fun getById(
|
||||||
messagesIds: List<Int>,
|
messagesIds: List<Int>,
|
||||||
extended: Boolean?,
|
extended: Boolean?,
|
||||||
fields: String?
|
fields: String?
|
||||||
): ApiResult<VkMessage, RestApiErrorDomain>
|
): ApiResult<List<VkMessage>, RestApiErrorDomain>
|
||||||
|
|
||||||
suspend fun send(
|
suspend fun send(
|
||||||
peerId: Int,
|
peerId: Int,
|
||||||
@@ -33,14 +33,16 @@ interface MessagesRepository {
|
|||||||
startMessageId: Int?
|
startMessageId: Int?
|
||||||
): ApiResult<Int, RestApiErrorDomain>
|
): 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 storeMessages(messages: List<VkMessage>)
|
||||||
|
|
||||||
// suspend fun getHistory(
|
|
||||||
// params: MessagesGetHistoryRequest
|
|
||||||
// ): ApiResult<MessagesGetHistoryResponse, RestApiErrorDomain>
|
|
||||||
|
|
||||||
// suspend fun markAsImportant(
|
// suspend fun markAsImportant(
|
||||||
// params: MessagesMarkAsImportantRequest
|
// params: MessagesMarkAsImportantRequest
|
||||||
// ): ApiResult<List<Int>, RestApiErrorDomain>
|
// ): ApiResult<List<Int>, RestApiErrorDomain>
|
||||||
|
|||||||
+153
-76
@@ -1,56 +1,132 @@
|
|||||||
package com.meloda.app.fast.data.api.messages
|
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.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.VkMessage
|
||||||
import com.meloda.app.fast.model.api.domain.asEntity
|
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.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 com.slack.eithernet.ApiResult
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
// TODO: 05/05/2024, Danil Nikolaev: implement syncing
|
|
||||||
class MessagesRepositoryImpl(
|
class MessagesRepositoryImpl(
|
||||||
private val networkDataSource: MessagesNetworkDataSource,
|
private val messagesService: MessagesService,
|
||||||
private val localDataSource: MessagesLocalDataSource
|
private val messageDao: MessageDao,
|
||||||
) : MessagesRepository {
|
) : MessagesRepository {
|
||||||
|
|
||||||
override suspend fun getMessagesHistory(
|
override suspend fun getHistory(
|
||||||
conversationId: Int,
|
conversationId: Int,
|
||||||
offset: Int?,
|
offset: Int?,
|
||||||
count: Int?
|
count: Int?
|
||||||
): ApiResult<MessagesHistoryDomain, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||||
// val localMessages = localDataSource.getMessages(
|
val requestModel = MessagesGetHistoryRequest(
|
||||||
// conversationId = conversationId,
|
count = count,
|
||||||
// offset = offset,
|
offset = offset,
|
||||||
// count = count
|
peerId = conversationId,
|
||||||
// ).map(VkMessageEntity::asExternalModel)
|
extended = true,
|
||||||
//
|
startMessageId = null,
|
||||||
// emit(localMessages)
|
rev = null,
|
||||||
//
|
fields = VkConstants.ALL_FIELDS
|
||||||
// val networkMessages = networkDataSource.getMessagesHistory(
|
)
|
||||||
// conversationId = conversationId,
|
|
||||||
// offset = offset,
|
|
||||||
// count = count
|
|
||||||
// )
|
|
||||||
//
|
|
||||||
// emit(networkMessages)
|
|
||||||
|
|
||||||
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>,
|
messagesIds: List<Int>,
|
||||||
extended: Boolean?,
|
extended: Boolean?,
|
||||||
fields: String?
|
fields: String?
|
||||||
): ApiResult<VkMessage, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
): ApiResult<List<VkMessage>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||||
networkDataSource.getMessageById(
|
val requestModel = MessagesGetByIdRequest(
|
||||||
messagesIds = messagesIds,
|
messagesIds = messagesIds,
|
||||||
extended = extended,
|
extended = extended,
|
||||||
fields = fields
|
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(
|
override suspend fun send(
|
||||||
@@ -60,54 +136,72 @@ class MessagesRepositoryImpl(
|
|||||||
replyTo: Int?,
|
replyTo: Int?,
|
||||||
attachments: List<VkAttachment>?
|
attachments: List<VkAttachment>?
|
||||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||||
networkDataSource.send(
|
val requestModel = MessagesSendRequest(
|
||||||
peerId,
|
peerId = peerId,
|
||||||
randomId,
|
randomId = randomId,
|
||||||
message,
|
message = message,
|
||||||
replyTo,
|
replyTo = replyTo,
|
||||||
attachments
|
attachments = attachments
|
||||||
)
|
)
|
||||||
|
|
||||||
|
messagesService.send(requestModel.map).mapApiDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun markAsRead(
|
override suspend fun markAsRead(
|
||||||
peerId: Int,
|
peerId: Int,
|
||||||
startMessageId: Int?
|
startMessageId: Int?
|
||||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
): 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 {
|
override suspend fun getHistoryAttachments(
|
||||||
val localMessage = localDataSource.getMessage(messageId)?.asExternalModel()
|
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>) {
|
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(
|
// override suspend fun markAsImportant(
|
||||||
// params: MessagesMarkAsImportantRequest
|
// params: MessagesMarkAsImportantRequest
|
||||||
// ): ApiResult<List<Int>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
// ): 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(
|
// override suspend fun getChat(
|
||||||
// params: MessagesGetChatRequest
|
// params: MessagesGetChatRequest
|
||||||
// ): ApiResult<VkChatData, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
// ): 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.data.State
|
||||||
import com.meloda.app.fast.model.api.domain.VkAttachment
|
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.VkMessage
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@@ -11,15 +12,9 @@ interface MessagesUseCase {
|
|||||||
conversationId: Int,
|
conversationId: Int,
|
||||||
count: Int?,
|
count: Int?,
|
||||||
offset: Int?
|
offset: Int?
|
||||||
): Flow<State<MessagesHistoryDomain>>
|
): Flow<State<MessagesHistoryInfo>>
|
||||||
|
|
||||||
fun getById(
|
fun getById(
|
||||||
messageId: Int,
|
|
||||||
extended: Boolean?,
|
|
||||||
fields: String?
|
|
||||||
): Flow<State<VkMessage?>>
|
|
||||||
|
|
||||||
fun getByIds(
|
|
||||||
messageIds: List<Int>,
|
messageIds: List<Int>,
|
||||||
extended: Boolean?,
|
extended: Boolean?,
|
||||||
fields: String?
|
fields: String?
|
||||||
@@ -38,6 +33,14 @@ interface MessagesUseCase {
|
|||||||
startMessageId: Int
|
startMessageId: Int
|
||||||
): Flow<State<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 storeMessage(message: VkMessage)
|
||||||
suspend fun storeMessages(messages: List<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.friends.FriendsRepositoryImpl
|
||||||
import com.meloda.app.fast.data.api.longpoll.LongPollRepository
|
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.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.MessagesRepository
|
||||||
import com.meloda.app.fast.data.api.messages.MessagesRepositoryImpl
|
import com.meloda.app.fast.data.api.messages.MessagesRepositoryImpl
|
||||||
import com.meloda.app.fast.data.api.oauth.OAuthRepository
|
import com.meloda.app.fast.data.api.oauth.OAuthRepository
|
||||||
@@ -59,8 +55,6 @@ val dataModule = module {
|
|||||||
|
|
||||||
singleOf(::LongPollRepositoryImpl) bind LongPollRepository::class
|
singleOf(::LongPollRepositoryImpl) bind LongPollRepository::class
|
||||||
|
|
||||||
singleOf(::MessagesLocalDataSourceImpl) bind MessagesLocalDataSource::class
|
|
||||||
singleOf(::MessagesNetworkDataSourceImpl) bind MessagesNetworkDataSource::class
|
|
||||||
singleOf(::MessagesRepositoryImpl) bind MessagesRepository::class
|
singleOf(::MessagesRepositoryImpl) bind MessagesRepository::class
|
||||||
|
|
||||||
singleOf(::OAuthRepositoryImpl) bind OAuthRepository::class
|
singleOf(::OAuthRepositoryImpl) bind OAuthRepository::class
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import com.meloda.app.fast.model.database.VkUserEntity
|
|||||||
VkConversationEntity::class
|
VkConversationEntity::class
|
||||||
],
|
],
|
||||||
|
|
||||||
version = 5
|
version = 6
|
||||||
)
|
)
|
||||||
@TypeConverters(Converters::class)
|
@TypeConverters(Converters::class)
|
||||||
abstract class CacheDatabase : RoomDatabase() {
|
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.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import com.meloda.app.fast.datastore.isUsingAmoledBackground
|
import com.meloda.app.fast.datastore.isUsingAmoledBackground
|
||||||
import com.meloda.app.fast.datastore.isUsingDynamicColors
|
import com.meloda.app.fast.datastore.isUsingDynamicColors
|
||||||
import com.meloda.app.fast.datastore.model.ThemeConfig
|
import com.meloda.app.fast.datastore.model.ThemeConfig
|
||||||
import com.meloda.app.fast.datastore.selectedColorScheme
|
import com.meloda.app.fast.datastore.selectedColorScheme
|
||||||
import com.meloda.app.fast.designsystem.colorschemes.ClassicColorScheme
|
import com.meloda.app.fast.designsystem.colorschemes.ClassicColorScheme
|
||||||
|
import dev.chrisbanes.haze.HazeState
|
||||||
|
|
||||||
private val googleSansFonts = FontFamily(
|
private val googleSansFonts = FontFamily(
|
||||||
Font(resId = R.font.google_sans_regular),
|
Font(resId = R.font.google_sans_regular),
|
||||||
@@ -115,6 +117,14 @@ val LocalTheme = compositionLocalOf {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val LocalHazeState = compositionLocalOf {
|
||||||
|
HazeState()
|
||||||
|
}
|
||||||
|
|
||||||
|
val LocalBottomPadding = compositionLocalOf {
|
||||||
|
0.dp
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppTheme(
|
fun AppTheme(
|
||||||
predefinedColorScheme: ColorScheme? = null,
|
predefinedColorScheme: ColorScheme? = null,
|
||||||
|
|||||||
+25
@@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
+29
-4
@@ -1,5 +1,7 @@
|
|||||||
package com.meloda.app.fast.model.api.data
|
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.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@@ -12,7 +14,7 @@ data class VkAttachmentItemData(
|
|||||||
@Json(name = "doc") val file: VkFileData?,
|
@Json(name = "doc") val file: VkFileData?,
|
||||||
@Json(name = "link") val link: VkLinkData?,
|
@Json(name = "link") val link: VkLinkData?,
|
||||||
@Json(name = "mini_app") val miniApp: VkMiniAppData?,
|
@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 = "sticker") val sticker: VkStickerData?,
|
||||||
@Json(name = "gift") val gift: VkGiftData?,
|
@Json(name = "gift") val gift: VkGiftData?,
|
||||||
@Json(name = "wall") val wall: VkWallData?,
|
@Json(name = "wall") val wall: VkWallData?,
|
||||||
@@ -20,7 +22,7 @@ data class VkAttachmentItemData(
|
|||||||
@Json(name = "poll") val poll: VkPollData?,
|
@Json(name = "poll") val poll: VkPollData?,
|
||||||
@Json(name = "wall_reply") val wallReply: VkWallReplyData?,
|
@Json(name = "wall_reply") val wallReply: VkWallReplyData?,
|
||||||
@Json(name = "call") val call: VkCallData?,
|
@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 = "curator") val curator: VkCuratorData?,
|
||||||
@Json(name = "event") val event: VkEventData?,
|
@Json(name = "event") val event: VkEventData?,
|
||||||
@Json(name = "story") val story: VkStoryData?,
|
@Json(name = "story") val story: VkStoryData?,
|
||||||
@@ -30,6 +32,29 @@ data class VkAttachmentItemData(
|
|||||||
@Json(name = "audio_playlist") val audioPlaylist: VkAudioPlaylistData?,
|
@Json(name = "audio_playlist") val audioPlaylist: VkAudioPlaylistData?,
|
||||||
@Json(name = "podcast") val podcast: VkPodcastData?
|
@Json(name = "podcast") val podcast: VkPodcastData?
|
||||||
) {
|
) {
|
||||||
|
fun toDomain(): VkAttachment = when (AttachmentType.parse(type)) {
|
||||||
fun getPreparedType(): AttachmentType = 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
|
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.meloda.app.fast.model.api.domain.VkMessage
|
||||||
import com.squareup.moshi.Json
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
@@ -60,6 +59,7 @@ data class VkMessageData(
|
|||||||
|
|
||||||
fun VkMessageData.asDomain(): VkMessage = VkMessage(
|
fun VkMessageData.asDomain(): VkMessage = VkMessage(
|
||||||
id = id ?: -1,
|
id = id ?: -1,
|
||||||
|
conversationMessageId = conversationMessageId,
|
||||||
text = text.ifBlank { null },
|
text = text.ifBlank { null },
|
||||||
isOut = out == 1,
|
isOut = out == 1,
|
||||||
peerId = peerId ?: -1,
|
peerId = peerId ?: -1,
|
||||||
@@ -75,134 +75,10 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage(
|
|||||||
important = important,
|
important = important,
|
||||||
updateTime = updateTime,
|
updateTime = updateTime,
|
||||||
forwards = fwdMessages.orEmpty().map(VkMessageData::asDomain),
|
forwards = fwdMessages.orEmpty().map(VkMessageData::asDomain),
|
||||||
attachments = parseAttachments(),
|
attachments = attachments.map(VkAttachmentItemData::toDomain),
|
||||||
replyMessage = replyMessage?.asDomain(),
|
replyMessage = replyMessage?.asDomain(),
|
||||||
user = null,
|
user = null,
|
||||||
group = null,
|
group = null,
|
||||||
actionUser = null,
|
actionUser = null,
|
||||||
actionGroup = 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(
|
fun mapToDomain(): VkMessage = VkMessage(
|
||||||
id = id ?: -1,
|
id = id ?: -1,
|
||||||
|
conversationMessageId = conversationMessageId,
|
||||||
text = text.ifBlank { null },
|
text = text.ifBlank { null },
|
||||||
isOut = out == true,
|
isOut = out == true,
|
||||||
peerId = peerId ?: -1,
|
peerId = peerId ?: -1,
|
||||||
|
|||||||
+10
@@ -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(
|
data class VkMessage(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
val conversationMessageId: Int,
|
||||||
val text: String?,
|
val text: String?,
|
||||||
val isOut: Boolean,
|
val isOut: Boolean,
|
||||||
val peerId: Int,
|
val peerId: Int,
|
||||||
@@ -78,6 +79,7 @@ data class VkMessage(
|
|||||||
|
|
||||||
fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity(
|
fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity(
|
||||||
id = id,
|
id = id,
|
||||||
|
conversationMessageId = conversationMessageId,
|
||||||
text = text,
|
text = text,
|
||||||
isOut = isOut,
|
isOut = isOut,
|
||||||
peerId = peerId,
|
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()
|
"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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+11
@@ -1,11 +1,13 @@
|
|||||||
package com.meloda.app.fast.model.api.responses
|
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.VkChatMemberData
|
||||||
import com.meloda.app.fast.model.api.data.VkContactData
|
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.VkConversationData
|
||||||
import com.meloda.app.fast.model.api.data.VkGroupData
|
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.VkMessageData
|
||||||
import com.meloda.app.fast.model.api.data.VkUserData
|
import com.meloda.app.fast.model.api.data.VkUserData
|
||||||
|
import com.squareup.moshi.Json
|
||||||
import com.squareup.moshi.JsonClass
|
import com.squareup.moshi.JsonClass
|
||||||
|
|
||||||
@JsonClass(generateAdapter = true)
|
@JsonClass(generateAdapter = true)
|
||||||
@@ -33,3 +35,12 @@ data class MessagesGetConversationMembersResponse(
|
|||||||
val profiles: List<VkUserData>?,
|
val profiles: List<VkUserData>?,
|
||||||
val groups: List<VkGroupData>?
|
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")
|
@Entity(tableName = "messages")
|
||||||
data class VkMessageEntity(
|
data class VkMessageEntity(
|
||||||
@PrimaryKey val id: Int,
|
@PrimaryKey val id: Int,
|
||||||
|
val conversationMessageId: Int,
|
||||||
val text: String?,
|
val text: String?,
|
||||||
val isOut: Boolean,
|
val isOut: Boolean,
|
||||||
val peerId: Int,
|
val peerId: Int,
|
||||||
@@ -29,6 +30,7 @@ data class VkMessageEntity(
|
|||||||
|
|
||||||
fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage(
|
fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage(
|
||||||
id = id,
|
id = id,
|
||||||
|
conversationMessageId = conversationMessageId,
|
||||||
text = text,
|
text = text,
|
||||||
isOut = isOut,
|
isOut = isOut,
|
||||||
peerId = peerId,
|
peerId = peerId,
|
||||||
|
|||||||
+7
@@ -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.data.VkLongPollData
|
||||||
import com.meloda.app.fast.model.api.responses.MessagesGetByIdResponse
|
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.model.api.responses.MessagesGetHistoryResponse
|
||||||
import com.meloda.app.fast.network.ApiResponse
|
import com.meloda.app.fast.network.ApiResponse
|
||||||
import com.meloda.app.fast.network.RestApiError
|
import com.meloda.app.fast.network.RestApiError
|
||||||
@@ -42,6 +43,12 @@ interface MessagesService {
|
|||||||
@FieldMap params: Map<String, String>
|
@FieldMap params: Map<String, String>
|
||||||
): ApiResult<ApiResponse<Int>, RestApiError>
|
): ApiResult<ApiResponse<Int>, RestApiError>
|
||||||
|
|
||||||
|
@FormUrlEncoded
|
||||||
|
@POST(MessagesUrls.GET_HISTORY_ATTACHMENTS)
|
||||||
|
suspend fun getHistoryAttachments(
|
||||||
|
@FieldMap params: Map<String, String>
|
||||||
|
): ApiResult<ApiResponse<MessagesGetHistoryAttachmentsResponse>, RestApiError>
|
||||||
|
|
||||||
// @FormUrlEncoded
|
// @FormUrlEncoded
|
||||||
// @POST(MessagesUrls.MarkAsImportant)
|
// @POST(MessagesUrls.MarkAsImportant)
|
||||||
// suspend fun markAsImportant(
|
// suspend fun markAsImportant(
|
||||||
|
|||||||
+1
@@ -18,4 +18,5 @@ object MessagesUrls {
|
|||||||
const val GET_CHAT = "${AppConstants.URL_API}/messages.getChat"
|
const val GET_CHAT = "${AppConstants.URL_API}/messages.getChat"
|
||||||
const val GET_CONVERSATIONS_MEMBERS = "${AppConstants.URL_API}/messages.getConversationMembers"
|
const val GET_CONVERSATIONS_MEMBERS = "${AppConstants.URL_API}/messages.getConversationMembers"
|
||||||
const val REMOVE_CHAT_USER = "${AppConstants.URL_API}/messages.removeChatUser"
|
const val REMOVE_CHAT_USER = "${AppConstants.URL_API}/messages.removeChatUser"
|
||||||
|
const val GET_HISTORY_ATTACHMENTS = "${AppConstants.URL_API}/messages.getHistoryAttachments"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ android {
|
|||||||
}
|
}
|
||||||
kotlinOptions {
|
kotlinOptions {
|
||||||
jvmTarget = Configs.java.toString()
|
jvmTarget = Configs.java.toString()
|
||||||
|
freeCompilerArgs = listOf("-opt-in=kotlin.RequiresOptIn", "-Xcontext-receivers")
|
||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
|||||||
-91
@@ -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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+130
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
@@ -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)
|
||||||
|
}
|
||||||
+24
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
+28
@@ -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()
|
||||||
|
}
|
||||||
+19
-4
@@ -1,13 +1,23 @@
|
|||||||
package com.meloda.app.fast.chatmaterials.navigation
|
package com.meloda.app.fast.chatmaterials.navigation
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.compose.composable
|
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
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
@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(
|
fun NavGraphBuilder.chatMaterialsRoute(
|
||||||
onBack: () -> Unit
|
onBack: () -> Unit
|
||||||
@@ -19,6 +29,11 @@ fun NavGraphBuilder.chatMaterialsRoute(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun NavController.navigateToChatMaterials() {
|
fun NavController.navigateToChatMaterials(peerId: Int, conversationMessageId: Int) {
|
||||||
this.navigate(ChatMaterials(""))
|
this.navigate(
|
||||||
|
ChatMaterials(
|
||||||
|
peerId = peerId,
|
||||||
|
conversationMessageId = conversationMessageId
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+68
@@ -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 -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
+352
@@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -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")
|
||||||
|
}
|
||||||
+7
@@ -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.ConversationOption
|
||||||
import com.meloda.app.fast.conversations.model.ConversationsScreenState
|
import com.meloda.app.fast.conversations.model.ConversationsScreenState
|
||||||
import com.meloda.app.fast.conversations.model.UiConversation
|
import com.meloda.app.fast.conversations.model.UiConversation
|
||||||
|
import com.meloda.app.fast.designsystem.LocalBottomPadding
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -45,6 +46,8 @@ fun ConversationsListComposable(
|
|||||||
|
|
||||||
val conversations = screenState.conversations
|
val conversations = screenState.conversations
|
||||||
|
|
||||||
|
val bottomPadding = LocalBottomPadding.current
|
||||||
|
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
state = state
|
state = state
|
||||||
@@ -105,5 +108,9 @@ fun ConversationsListComposable(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Spacer(modifier = Modifier.height(bottomPadding))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-3
@@ -5,16 +5,20 @@ import androidx.compose.animation.animateColorAsState
|
|||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
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.slideIn
|
||||||
import androidx.compose.animation.slideOut
|
import androidx.compose.animation.slideOut
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
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.IntOffset
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
import androidx.core.view.HapticFeedbackConstantsCompat
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil.imageLoader
|
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.ConversationsViewModelImpl
|
||||||
import com.meloda.app.fast.conversations.model.ConversationsScreenState
|
import com.meloda.app.fast.conversations.model.ConversationsScreenState
|
||||||
import com.meloda.app.fast.conversations.model.UiConversation
|
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.LocalTheme
|
||||||
import com.meloda.app.fast.designsystem.MaterialDialog
|
import com.meloda.app.fast.designsystem.MaterialDialog
|
||||||
import com.meloda.app.fast.designsystem.components.FullScreenLoader
|
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 {
|
var dropDownMenuExpanded by remember {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
@@ -252,11 +260,12 @@ fun ConversationsScreen(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val rotation = remember { Animatable(0f) }
|
val rotation = remember { Animatable(0f) }
|
||||||
|
|
||||||
|
Column {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = isListScrollingUp,
|
visible = isListScrollingUp,
|
||||||
modifier = Modifier.navigationBarsPadding(),
|
modifier = Modifier.navigationBarsPadding(),
|
||||||
enter = slideIn { IntOffset(0, 400) },
|
enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
|
||||||
exit = slideOut { IntOffset(0, 400) }
|
exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
|
||||||
) {
|
) {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -285,6 +294,9 @@ fun ConversationsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(LocalBottomPadding.current))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
when {
|
when {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ class FriendsViewModelImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun loadFriends(offset: Int = currentOffset.value) {
|
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(
|
state.processState(
|
||||||
error = { error ->
|
error = { error ->
|
||||||
when (error) {
|
when (error) {
|
||||||
@@ -103,11 +103,11 @@ class FriendsViewModelImpl(
|
|||||||
},
|
},
|
||||||
success = { info ->
|
success = { info ->
|
||||||
val response = info.friends
|
val response = info.friends
|
||||||
val itemsCountSufficient = response.size == 30
|
val itemsCountSufficient = response.size == LOAD_COUNT
|
||||||
canPaginate.setValue { itemsCountSufficient }
|
canPaginate.setValue { itemsCountSufficient }
|
||||||
|
|
||||||
val paginationExhausted = !itemsCountSufficient &&
|
val paginationExhausted = !itemsCountSufficient &&
|
||||||
screenState.value.friends.size >= 30
|
screenState.value.friends.size >= LOAD_COUNT
|
||||||
|
|
||||||
imagesToPreload.setValue {
|
imagesToPreload.setValue {
|
||||||
response.mapNotNull(VkUser::photo100)
|
response.mapNotNull(VkUser::photo100)
|
||||||
@@ -172,4 +172,8 @@ class FriendsViewModelImpl(
|
|||||||
}
|
}
|
||||||
uiOnlineFriends.setValue { onlineUiFriends }
|
uiOnlineFriends.setValue { onlineUiFriends }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val LOAD_COUNT = 30
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -354,6 +354,7 @@ class MessagesHistoryViewModelImpl(
|
|||||||
|
|
||||||
val newMessage = VkMessage(
|
val newMessage = VkMessage(
|
||||||
id = -1 - sendingMessages.size,
|
id = -1 - sendingMessages.size,
|
||||||
|
conversationMessageId = -1,
|
||||||
text = lastMessageText,
|
text = lastMessageText,
|
||||||
isOut = true,
|
isOut = true,
|
||||||
peerId = screenState.value.conversationId,
|
peerId = screenState.value.conversationId,
|
||||||
|
|||||||
+40
-59
@@ -1,27 +1,28 @@
|
|||||||
package com.meloda.app.fast.messageshistory.domain
|
package com.meloda.app.fast.messageshistory.domain
|
||||||
|
|
||||||
import com.meloda.app.fast.data.State
|
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.MessagesRepository
|
||||||
import com.meloda.app.fast.data.api.messages.MessagesUseCase
|
import com.meloda.app.fast.data.api.messages.MessagesUseCase
|
||||||
import com.meloda.app.fast.data.mapToState
|
import com.meloda.app.fast.data.mapToState
|
||||||
import com.meloda.app.fast.model.api.domain.VkAttachment
|
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.VkMessage
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
|
||||||
class MessagesUseCaseImpl(
|
class MessagesUseCaseImpl(
|
||||||
private val messagesRepository: MessagesRepository
|
private val repository: MessagesRepository
|
||||||
) : MessagesUseCase {
|
) : MessagesUseCase {
|
||||||
|
|
||||||
override fun getMessagesHistory(
|
override fun getMessagesHistory(
|
||||||
conversationId: Int,
|
conversationId: Int,
|
||||||
count: Int?,
|
count: Int?,
|
||||||
offset: Int?
|
offset: Int?
|
||||||
): Flow<State<MessagesHistoryDomain>> = flow {
|
): Flow<State<MessagesHistoryInfo>> = flow {
|
||||||
emit(State.Loading)
|
emit(State.Loading)
|
||||||
|
|
||||||
val newState = messagesRepository.getMessagesHistory(
|
val newState = repository.getHistory(
|
||||||
conversationId = conversationId,
|
conversationId = conversationId,
|
||||||
offset = offset,
|
offset = offset,
|
||||||
count = count
|
count = count
|
||||||
@@ -31,60 +32,20 @@ class MessagesUseCaseImpl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getById(
|
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>,
|
messageIds: List<Int>,
|
||||||
extended: Boolean?,
|
extended: Boolean?,
|
||||||
fields: String?
|
fields: String?
|
||||||
): Flow<State<List<VkMessage>>> = flow {}
|
): Flow<State<List<VkMessage>>> = flow {
|
||||||
// flow {
|
emit(State.Loading)
|
||||||
// emit(State.Loading)
|
|
||||||
//
|
val newState = repository.getById(
|
||||||
// val newState = messagesRepository.getById(
|
messagesIds = messageIds,
|
||||||
// params = MessagesGetByIdRequest(
|
extended = extended,
|
||||||
// messagesIds = messageIds,
|
fields = fields
|
||||||
// extended = extended,
|
).mapToState()
|
||||||
// fields = fields
|
|
||||||
// )
|
emit(newState)
|
||||||
// ).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)
|
|
||||||
// }
|
|
||||||
|
|
||||||
override fun sendMessage(
|
override fun sendMessage(
|
||||||
peerId: Int,
|
peerId: Int,
|
||||||
@@ -95,7 +56,7 @@ class MessagesUseCaseImpl(
|
|||||||
): Flow<State<Int>> = flow {
|
): Flow<State<Int>> = flow {
|
||||||
emit(State.Loading)
|
emit(State.Loading)
|
||||||
|
|
||||||
val newState = messagesRepository.send(
|
val newState = repository.send(
|
||||||
peerId = peerId,
|
peerId = peerId,
|
||||||
randomId = randomId,
|
randomId = randomId,
|
||||||
message = message,
|
message = message,
|
||||||
@@ -112,7 +73,7 @@ class MessagesUseCaseImpl(
|
|||||||
): Flow<State<Int>> = flow {
|
): Flow<State<Int>> = flow {
|
||||||
emit(State.Loading)
|
emit(State.Loading)
|
||||||
|
|
||||||
val newState = messagesRepository.markAsRead(
|
val newState = repository.markAsRead(
|
||||||
peerId = peerId,
|
peerId = peerId,
|
||||||
startMessageId = startMessageId
|
startMessageId = startMessageId
|
||||||
).mapToState()
|
).mapToState()
|
||||||
@@ -120,11 +81,31 @@ class MessagesUseCaseImpl(
|
|||||||
emit(newState)
|
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) {
|
override suspend fun storeMessage(message: VkMessage) {
|
||||||
messagesRepository.storeMessages(listOf(message))
|
repository.storeMessages(listOf(message))
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun storeMessages(messages: List<VkMessage>) {
|
override suspend fun storeMessages(messages: List<VkMessage>) {
|
||||||
messagesRepository.storeMessages(messages)
|
repository.storeMessages(messages)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
@@ -4,6 +4,7 @@ import com.meloda.app.fast.common.UiImage
|
|||||||
|
|
||||||
data class UiMessage(
|
data class UiMessage(
|
||||||
val id: Int,
|
val id: Int,
|
||||||
|
val conversationMessageId: Int,
|
||||||
val text: String?,
|
val text: String?,
|
||||||
val isOut: Boolean,
|
val isOut: Boolean,
|
||||||
val fromId: Int,
|
val fromId: Int,
|
||||||
|
|||||||
+1
-1
@@ -40,7 +40,7 @@ val MessagesHistoryNavType = object : NavType<MessagesHistoryArguments>(isNullab
|
|||||||
fun NavGraphBuilder.messagesHistoryRoute(
|
fun NavGraphBuilder.messagesHistoryRoute(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onNavigateToChatAttachments: () -> Unit
|
onNavigateToChatAttachments: (peerId: Int, conversationMessageId: Int) -> Unit
|
||||||
) {
|
) {
|
||||||
composable<MessagesHistory>(
|
composable<MessagesHistory>(
|
||||||
typeMap = mapOf(typeOf<MessagesHistoryArguments>() to MessagesHistoryNavType)
|
typeMap = mapOf(typeOf<MessagesHistoryArguments>() to MessagesHistoryNavType)
|
||||||
|
|||||||
+7
-2
@@ -90,7 +90,7 @@ import com.meloda.app.fast.designsystem.R as UiR
|
|||||||
fun MessagesHistoryScreen(
|
fun MessagesHistoryScreen(
|
||||||
onError: (BaseError) -> Unit,
|
onError: (BaseError) -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onNavigateToChatMaterials: () -> Unit,
|
onNavigateToChatMaterials: (peerId: Int, conversationMessageId: Int) -> Unit,
|
||||||
viewModel: MessagesHistoryViewModel = koinViewModel<MessagesHistoryViewModelImpl>()
|
viewModel: MessagesHistoryViewModel = koinViewModel<MessagesHistoryViewModelImpl>()
|
||||||
) {
|
) {
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
@@ -215,7 +215,12 @@ fun MessagesHistoryScreen(
|
|||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
dropDownMenuExpanded = false
|
dropDownMenuExpanded = false
|
||||||
onNavigateToChatMaterials()
|
|
||||||
|
// TODO: 11/07/2024, Danil Nikolaev: to VM
|
||||||
|
onNavigateToChatMaterials(
|
||||||
|
screenState.conversationId,
|
||||||
|
screenState.messages.first().conversationMessageId
|
||||||
|
)
|
||||||
},
|
},
|
||||||
text = {
|
text = {
|
||||||
Text(text = "Materials")
|
Text(text = "Materials")
|
||||||
|
|||||||
+1
@@ -91,6 +91,7 @@ fun VkMessage.asPresentation(
|
|||||||
nextMessage: VkMessage?
|
nextMessage: VkMessage?
|
||||||
): UiMessage = UiMessage(
|
): UiMessage = UiMessage(
|
||||||
id = id,
|
id = id,
|
||||||
|
conversationMessageId = conversationMessageId,
|
||||||
text = text,
|
text = text,
|
||||||
isOut = isOut,
|
isOut = isOut,
|
||||||
fromId = fromId,
|
fromId = fromId,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
agp = "8.5.0"
|
agp = "8.5.0"
|
||||||
converterMoshi = "2.11.0"
|
converterMoshi = "2.11.0"
|
||||||
eithernet = "1.9.0"
|
eithernet = "1.9.0"
|
||||||
haze = "0.7.2"
|
haze = "0.7.3"
|
||||||
kotlin = "2.0.0"
|
kotlin = "2.0.0"
|
||||||
ksp = "2.0.0-1.0.22"
|
ksp = "2.0.0-1.0.22"
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ nanokt = "1.2.0"
|
|||||||
junitVersion = "1.2.1"
|
junitVersion = "1.2.1"
|
||||||
espressoCore = "3.6.1"
|
espressoCore = "3.6.1"
|
||||||
appcompat = "1.7.0"
|
appcompat = "1.7.0"
|
||||||
androidx-navigation = "2.8.0-beta04"
|
androidx-navigation = "2.8.0-beta05"
|
||||||
serialization = "1.7.1"
|
serialization = "1.7.1"
|
||||||
|
|
||||||
[libraries]
|
[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-junit4 = { module = "androidx.compose.ui:ui-test-junit4" }
|
||||||
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" }
|
||||||
eithernet = { module = "com.slack.eithernet:eithernet", version.ref = "eithernet" }
|
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 = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
|
||||||
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
|
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }
|
||||||
junit = { module = "junit:junit", version.ref = "junit" }
|
junit = { module = "junit:junit", version.ref = "junit" }
|
||||||
|
|||||||
Reference in New Issue
Block a user