* refactor Conversation -> Convo

* extract Message and Convo mappers to core/domain module
* improve reply container text
This commit is contained in:
2025-12-17 17:16:02 +03:00
parent 7b6571f208
commit 45ee0acea5
125 changed files with 2361 additions and 2005 deletions
+1 -1
View File
@@ -82,7 +82,7 @@ dependencies {
implementation(projects.feature.auth) implementation(projects.feature.auth)
implementation(projects.feature.chatmaterials) implementation(projects.feature.chatmaterials)
implementation(projects.feature.conversations) implementation(projects.feature.convos)
implementation(projects.feature.languagepicker) implementation(projects.feature.languagepicker)
implementation(projects.feature.messageshistory) implementation(projects.feature.messageshistory)
implementation(projects.feature.photoviewer) implementation(projects.feature.photoviewer)
@@ -16,8 +16,8 @@ import dev.meloda.fast.common.LongPollControllerImpl
import dev.meloda.fast.common.provider.Provider import dev.meloda.fast.common.provider.Provider
import dev.meloda.fast.common.provider.ResourceProvider import dev.meloda.fast.common.provider.ResourceProvider
import dev.meloda.fast.common.provider.ResourceProviderImpl import dev.meloda.fast.common.provider.ResourceProviderImpl
import dev.meloda.fast.conversations.di.conversationsModule import dev.meloda.fast.convos.di.convosModule
import dev.meloda.fast.conversations.di.createChatModule import dev.meloda.fast.convos.di.createChatModule
import dev.meloda.fast.domain.di.domainModule import dev.meloda.fast.domain.di.domainModule
import dev.meloda.fast.friends.di.friendsModule import dev.meloda.fast.friends.di.friendsModule
import dev.meloda.fast.languagepicker.di.languagePickerModule import dev.meloda.fast.languagepicker.di.languagePickerModule
@@ -41,7 +41,7 @@ val applicationModule = module {
loginModule, loginModule,
validationModule, validationModule,
captchaModule, captchaModule,
conversationsModule, convosModule,
settingsModule, settingsModule,
messagesHistoryModule, messagesHistoryModule,
photoViewModule, photoViewModule,
@@ -2,7 +2,7 @@ package dev.meloda.fast.navigation
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import dev.meloda.fast.conversations.navigation.ConversationsGraph import dev.meloda.fast.convos.navigation.ConvoGraph
import dev.meloda.fast.friends.navigation.Friends import dev.meloda.fast.friends.navigation.Friends
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.BottomNavigationItem import dev.meloda.fast.model.BottomNavigationItem
@@ -21,7 +21,7 @@ object Main
fun NavGraphBuilder.mainScreen( fun NavGraphBuilder.mainScreen(
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onSettingsButtonClicked: () -> Unit, onSettingsButtonClicked: () -> Unit,
onNavigateToMessagesHistory: (conversationId: Long) -> Unit, onNavigateToMessagesHistory: (convoId: Long) -> Unit,
onPhotoClicked: (url: String) -> Unit, onPhotoClicked: (url: String) -> Unit,
onMessageClicked: (userid: Long) -> Unit, onMessageClicked: (userid: Long) -> Unit,
onNavigateToCreateChat: () -> Unit onNavigateToCreateChat: () -> Unit
@@ -34,10 +34,10 @@ fun NavGraphBuilder.mainScreen(
route = Friends, route = Friends,
), ),
BottomNavigationItem( BottomNavigationItem(
titleResId = R.string.title_conversations, titleResId = R.string.title_convos,
selectedIconResId = R.drawable.baseline_chat_24, selectedIconResId = R.drawable.baseline_chat_24,
unselectedIconResId = R.drawable.outline_chat_24, unselectedIconResId = R.drawable.outline_chat_24,
route = ConversationsGraph route = ConvoGraph
), ),
BottomNavigationItem( BottomNavigationItem(
titleResId = R.string.title_profile, titleResId = R.string.title_profile,
@@ -38,8 +38,8 @@ import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.conversations.navigation.ConversationsGraph import dev.meloda.fast.convos.navigation.ConvoGraph
import dev.meloda.fast.conversations.navigation.conversationsGraph import dev.meloda.fast.convos.navigation.convosGraph
import dev.meloda.fast.friends.navigation.Friends import dev.meloda.fast.friends.navigation.Friends
import dev.meloda.fast.friends.navigation.friendsScreen import dev.meloda.fast.friends.navigation.friendsScreen
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
@@ -60,7 +60,7 @@ fun MainScreen(
navigationItems: ImmutableList<BottomNavigationItem>, navigationItems: ImmutableList<BottomNavigationItem>,
onError: (BaseError) -> Unit = {}, onError: (BaseError) -> Unit = {},
onSettingsButtonClicked: () -> Unit = {}, onSettingsButtonClicked: () -> Unit = {},
onNavigateToMessagesHistory: (conversationId: Long) -> Unit = {}, onNavigateToMessagesHistory: (convoId: Long) -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {}, onPhotoClicked: (url: String) -> Unit = {},
onMessageClicked: (userid: Long) -> Unit = {}, onMessageClicked: (userid: Long) -> Unit = {},
onNavigateToCreateChat: () -> Unit = {} onNavigateToCreateChat: () -> Unit = {}
@@ -197,14 +197,14 @@ fun MainScreen(
} }
}, },
) )
conversationsGraph( convosGraph(
activity = activity, activity = activity,
onError = onError, onError = onError,
onNavigateToMessagesHistory = onNavigateToMessagesHistory, onNavigateToMessagesHistory = onNavigateToMessagesHistory,
onNavigateToCreateChat = onNavigateToCreateChat, onNavigateToCreateChat = onNavigateToCreateChat,
onScrolledToTop = { onScrolledToTop = {
tabReselected = tabReselected.toMutableMap().also { tabReselected = tabReselected.toMutableMap().also {
it[ConversationsGraph] = false it[ConvoGraph] = false
} }
} }
) )
@@ -46,8 +46,8 @@ import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen
import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials
import dev.meloda.fast.common.LongPollController import dev.meloda.fast.common.LongPollController
import dev.meloda.fast.common.model.LongPollState import dev.meloda.fast.common.model.LongPollState
import dev.meloda.fast.conversations.navigation.createChatScreen import dev.meloda.fast.convos.navigation.createChatScreen
import dev.meloda.fast.conversations.navigation.navigateToCreateChat import dev.meloda.fast.convos.navigation.navigateToCreateChat
import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.languagepicker.navigation.languagePickerScreen import dev.meloda.fast.languagepicker.navigation.languagePickerScreen
import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker
@@ -354,9 +354,9 @@ fun RootScreen(
} }
) )
createChatScreen( createChatScreen(
onChatCreated = { conversationId -> onChatCreated = { convoId ->
navController.popBackStack() navController.popBackStack()
navController.navigateToMessagesHistory(conversationId) navController.navigateToMessagesHistory(convoId)
}, },
navController = navController navController = navController
) )
@@ -55,7 +55,8 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() =
"-opt-in=kotlin.RequiresOptIn", "-opt-in=kotlin.RequiresOptIn",
// Enable experimental coroutines APIs, including Flow // Enable experimental coroutines APIs, including Flow
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-opt-in=kotlinx.coroutines.FlowPreview" "-opt-in=kotlinx.coroutines.FlowPreview",
"-Xannotation-default-target=param-property",
) )
} }
} }
@@ -138,3 +138,17 @@ fun <T : Any> Bundle.getParcelableCompat(key: String, clazz: KClass<T>): T? {
getParcelable(key) getParcelable(key)
} }
} }
infix fun ClosedRange<Int>.collidesWith(other: ClosedRange<Int>): Boolean {
return this.start < other.endInclusive && other.start < this.endInclusive
}
fun dickPizda(a: Int): String = ""
operator fun ClosedRange<Int>.minus(other: ClosedRange<Int>): ClosedRange<Int> {
return (this.start - other.start)..(this.endInclusive - other.endInclusive)
}
operator fun ClosedRange<Int>.minus(other: Int): ClosedRange<Int> {
return (this.start - other)..(this.endInclusive - other)
}
@@ -1,7 +1,7 @@
package dev.meloda.fast.data package dev.meloda.fast.data
import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.data.VkMessageData
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkGroupDomain import dev.meloda.fast.model.api.domain.VkGroupDomain
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import kotlin.math.abs import kotlin.math.abs
@@ -16,9 +16,9 @@ class VkGroupsMap(
fun groups(): List<VkGroupDomain> = map.values.toList() fun groups(): List<VkGroupDomain> = map.values.toList()
fun conversationGroup(conversation: VkConversation): VkGroupDomain? = fun convoGroup(convo: VkConvo): VkGroupDomain? =
if (!conversation.peerType.isGroup()) null if (!convo.peerType.isGroup()) null
else map[abs(conversation.id)] else map[abs(convo.id)]
fun messageActionGroup(message: VkMessage): VkGroupDomain? = fun messageActionGroup(message: VkMessage): VkGroupDomain? =
if (message.actionMemberId == null || message.actionMemberId!! >= 0) null if (message.actionMemberId == null || message.actionMemberId!! >= 0) null
@@ -2,7 +2,7 @@ package dev.meloda.fast.data
import dev.meloda.fast.data.UserConfig.userId import dev.meloda.fast.data.UserConfig.userId
import dev.meloda.fast.model.api.domain.VkContactDomain import dev.meloda.fast.model.api.domain.VkContactDomain
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkGroupDomain import dev.meloda.fast.model.api.domain.VkGroupDomain
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
@@ -13,7 +13,7 @@ object VkMemoryCache {
private val users: HashMap<Long, VkUser> = hashMapOf() private val users: HashMap<Long, VkUser> = hashMapOf()
private val groups: HashMap<Long, VkGroupDomain> = hashMapOf() private val groups: HashMap<Long, VkGroupDomain> = hashMapOf()
private val messages: HashMap<Long, VkMessage> = hashMapOf() private val messages: HashMap<Long, VkMessage> = hashMapOf()
private val conversations: HashMap<Long, VkConversation> = hashMapOf() private val convos: HashMap<Long, VkConvo> = hashMapOf()
private val contacts: HashMap<Long, VkContactDomain> = hashMapOf() private val contacts: HashMap<Long, VkContactDomain> = hashMapOf()
fun appendUsers(users: List<VkUser>) { fun appendUsers(users: List<VkUser>) {
@@ -28,9 +28,9 @@ object VkMemoryCache {
messages.forEach { message -> VkMemoryCache.messages[message.id] = message } messages.forEach { message -> VkMemoryCache.messages[message.id] = message }
} }
fun appendConversations(conversations: List<VkConversation>) { fun appendConvos(convos: List<VkConvo>) {
conversations.forEach { conversation -> convos.forEach { convo ->
VkMemoryCache.conversations[conversation.id] = conversation VkMemoryCache.convos[convo.id] = convo
} }
} }
@@ -50,8 +50,8 @@ object VkMemoryCache {
messages[messageId] = message messages[messageId] = message
} }
operator fun set(conversationId: Long, conversation: VkConversation) { operator fun set(convoId: Long, convo: VkConvo) {
conversations[conversationId] = conversation convos[convoId] = convo
} }
operator fun set(contactId: Long, contact: VkContactDomain) { operator fun set(contactId: Long, contact: VkContactDomain) {
@@ -94,16 +94,16 @@ object VkMemoryCache {
return ids.mapNotNull { id -> messages[id] } return ids.mapNotNull { id -> messages[id] }
} }
fun getConversation(id: Long): VkConversation? { fun getConvo(id: Long): VkConvo? {
return getConversations(id).firstOrNull() return getConvos(id).firstOrNull()
} }
fun getConversations(vararg ids: Long): List<VkConversation> { fun getConvos(vararg ids: Long): List<VkConvo> {
return getConversations(ids.toList()) return getConvos(ids.toList())
} }
fun getConversations(ids: List<Long>): List<VkConversation> { fun getConvos(ids: List<Long>): List<VkConvo> {
return ids.mapNotNull { id -> conversations[id] } return ids.mapNotNull { id -> convos[id] }
} }
fun getContact(id: Long): VkContactDomain? { fun getContact(id: Long): VkContactDomain? {
@@ -1,8 +1,7 @@
package dev.meloda.fast.data package dev.meloda.fast.data
import dev.meloda.fast.data.UserConfig.userId
import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.data.VkMessageData
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
@@ -16,9 +15,9 @@ class VkUsersMap(
fun users(): List<VkUser> = map.values.toList() fun users(): List<VkUser> = map.values.toList()
fun conversationUser(conversation: VkConversation): VkUser? = fun convoUser(convo: VkConvo): VkUser? =
if (!conversation.peerType.isUser()) null if (!convo.peerType.isUser()) null
else map[conversation.id] else map[convo.id]
fun messageActionUser(message: VkMessage): VkUser? = fun messageActionUser(message: VkMessage): VkUser? =
if (message.actionMemberId == null || message.actionMemberId!! <= 0) null if (message.actionMemberId == null || message.actionMemberId!! <= 0) null
@@ -1,25 +1,25 @@
package dev.meloda.fast.data.api.conversations package dev.meloda.fast.data.api.convos
import com.slack.eithernet.ApiResult import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.ConversationsFilter import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
interface ConversationsRepository { interface ConvosRepository {
suspend fun storeConversations(conversations: List<VkConversation>) suspend fun storeConvos(convos: List<VkConvo>)
suspend fun getConversations( suspend fun getConvos(
count: Int?, count: Int?,
offset: Int?, offset: Int?,
filter: ConversationsFilter filter: ConvosFilter
): ApiResult<List<VkConversation>, RestApiErrorDomain> ): ApiResult<List<VkConvo>, RestApiErrorDomain>
suspend fun getConversationsById( suspend fun getConvosById(
peerIds: List<Long>, peerIds: List<Long>,
extended: Boolean? = null, extended: Boolean? = null,
fields: String? = null fields: String? = null
): ApiResult<List<VkConversation>, RestApiErrorDomain> ): ApiResult<List<VkConvo>, RestApiErrorDomain>
suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain> suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain>
suspend fun pin(peerId: Long): ApiResult<Int, RestApiErrorDomain> suspend fun pin(peerId: Long): ApiResult<Int, RestApiErrorDomain>
@@ -1,51 +1,51 @@
package dev.meloda.fast.data.api.conversations package dev.meloda.fast.data.api.convos
import com.slack.eithernet.ApiResult import com.slack.eithernet.ApiResult
import dev.meloda.fast.common.VkConstants import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.data.VkGroupsMap import dev.meloda.fast.data.VkGroupsMap
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.data.VkUsersMap import dev.meloda.fast.data.VkUsersMap
import dev.meloda.fast.database.dao.ConversationDao import dev.meloda.fast.database.dao.ConvoDao
import dev.meloda.fast.database.dao.GroupDao import dev.meloda.fast.database.dao.GroupDao
import dev.meloda.fast.database.dao.MessageDao import dev.meloda.fast.database.dao.MessageDao
import dev.meloda.fast.database.dao.UserDao import dev.meloda.fast.database.dao.UserDao
import dev.meloda.fast.model.ConversationsFilter import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.data.VkContactData import dev.meloda.fast.model.api.data.VkContactData
import dev.meloda.fast.model.api.data.VkGroupData import dev.meloda.fast.model.api.data.VkGroupData
import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.data.VkUserData
import dev.meloda.fast.model.api.data.asDomain import dev.meloda.fast.model.api.data.asDomain
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkGroupDomain import dev.meloda.fast.model.api.domain.VkGroupDomain
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.model.api.domain.asEntity import dev.meloda.fast.model.api.domain.asEntity
import dev.meloda.fast.model.api.requests.ConversationsGetRequest import dev.meloda.fast.model.api.requests.ConvosGetRequest
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.mapApiDefault
import dev.meloda.fast.network.mapApiResult import dev.meloda.fast.network.mapApiResult
import dev.meloda.fast.network.service.conversations.ConversationsService import dev.meloda.fast.network.service.convos.ConvosService
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class ConversationsRepositoryImpl( class ConvosRepositoryImpl(
private val conversationsService: ConversationsService, private val convosService: ConvosService,
private val messageDao: MessageDao, private val messageDao: MessageDao,
private val userDao: UserDao, private val userDao: UserDao,
private val groupDao: GroupDao, private val groupDao: GroupDao,
private val conversationDao: ConversationDao private val convoDao: ConvoDao
) : ConversationsRepository { ) : ConvosRepository {
override suspend fun storeConversations(conversations: List<VkConversation>) { override suspend fun storeConvos(convos: List<VkConvo>) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity)) convoDao.insertAll(convos.map(VkConvo::asEntity))
} }
override suspend fun getConversations( override suspend fun getConvos(
count: Int?, count: Int?,
offset: Int?, offset: Int?,
filter: ConversationsFilter filter: ConvosFilter
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<VkConvo>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConversationsGetRequest( val requestModel = ConvosGetRequest(
count = count, count = count,
offset = offset, offset = offset,
fields = VkConstants.ALL_FIELDS, fields = VkConstants.ALL_FIELDS,
@@ -54,7 +54,7 @@ class ConversationsRepositoryImpl(
startMessageId = null startMessageId = null
) )
conversationsService.getConversations(requestModel.map).mapApiResult( convosService.getConvos(requestModel.map).mapApiResult(
successMapper = { apiResponse -> successMapper = { apiResponse ->
val response = apiResponse.requireResponse() val response = apiResponse.requireResponse()
@@ -69,7 +69,7 @@ class ConversationsRepositoryImpl(
VkMemoryCache.appendGroups(groupsList) VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList) VkMemoryCache.appendContacts(contactsList)
val conversations = response.items.map { item -> val convos = response.items.map { item ->
val lastMessage = item.lastMessage?.asDomain()?.let { message -> val lastMessage = item.lastMessage?.asDomain()?.let { message ->
message.copy( message.copy(
user = usersMap.messageUser(message), user = usersMap.messageUser(message),
@@ -84,24 +84,24 @@ class ConversationsRepositoryImpl(
) )
).also { VkMemoryCache[message.id] = it } ).also { VkMemoryCache[message.id] = it }
} }
item.conversation.asDomain(lastMessage).let { conversation -> item.convo.asDomain(lastMessage).let { convo ->
conversation.copy( convo.copy(
user = usersMap.conversationUser(conversation), user = usersMap.convoUser(convo),
group = groupsMap.conversationGroup(conversation) group = groupsMap.convoGroup(convo)
).also { VkMemoryCache[conversation.id] = it } ).also { VkMemoryCache[convo.id] = it }
} }
} }
val messages = conversations.mapNotNull(VkConversation::lastMessage) val messages = convos.mapNotNull(VkConvo::lastMessage)
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity)) convoDao.insertAll(convos.map(VkConvo::asEntity))
messageDao.insertAll(messages.map(VkMessage::asEntity)) messageDao.insertAll(messages.map(VkMessage::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity)) userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity)) groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
} }
conversations convos
}, },
errorMapper = { error -> errorMapper = { error ->
error?.toDomain() error?.toDomain()
@@ -109,11 +109,11 @@ class ConversationsRepositoryImpl(
) )
} }
override suspend fun getConversationsById( override suspend fun getConvosById(
peerIds: List<Long>, peerIds: List<Long>,
extended: Boolean?, extended: Boolean?,
fields: String? fields: String?
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<List<VkConvo>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestParams = mutableMapOf( val requestParams = mutableMapOf(
"peer_ids" to peerIds.joinToString(separator = ",") "peer_ids" to peerIds.joinToString(separator = ",")
).apply { ).apply {
@@ -121,7 +121,7 @@ class ConversationsRepositoryImpl(
fields?.let { this["fields"] = it } fields?.let { this["fields"] = it }
} }
conversationsService.getConversationsById(requestParams).mapApiResult( convosService.getConvosById(requestParams).mapApiResult(
successMapper = { apiResponse -> successMapper = { apiResponse ->
val response = apiResponse.requireResponse() val response = apiResponse.requireResponse()
@@ -132,17 +132,17 @@ class ConversationsRepositoryImpl(
val usersMap = VkUsersMap.forUsers(profilesList) val usersMap = VkUsersMap.forUsers(profilesList)
val groupsMap = VkGroupsMap.forGroups(groupsList) val groupsMap = VkGroupsMap.forGroups(groupsList)
val conversations = response.items.map { item -> val convos = response.items.map { item ->
item.asDomain().let { conversation -> item.asDomain().let { convo ->
conversation.copy( convo.copy(
user = usersMap.conversationUser(conversation), user = usersMap.convoUser(convo),
group = groupsMap.conversationGroup(conversation) group = groupsMap.convoGroup(convo)
).also { VkMemoryCache[conversation.id] = it } ).also { VkMemoryCache[convo.id] = it }
} }
} }
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity)) convoDao.insertAll(convos.map(VkConvo::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity)) userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity)) groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
} }
@@ -151,7 +151,7 @@ class ConversationsRepositoryImpl(
VkMemoryCache.appendGroups(groupsList) VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList) VkMemoryCache.appendContacts(contactsList)
conversations convos
}, },
errorMapper = { error -> errorMapper = { error ->
error?.toDomain() error?.toDomain()
@@ -161,7 +161,7 @@ class ConversationsRepositoryImpl(
override suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain> = override suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
conversationsService.delete(mapOf("peer_id" to peerId.toString())).mapApiResult( convosService.delete(mapOf("peer_id" to peerId.toString())).mapApiResult(
successMapper = { response -> response.requireResponse().lastDeletedId }, successMapper = { response -> response.requireResponse().lastDeletedId },
errorMapper = { error -> error?.toDomain() } errorMapper = { error -> error?.toDomain() }
) )
@@ -170,19 +170,19 @@ class ConversationsRepositoryImpl(
override suspend fun pin( override suspend fun pin(
peerId: Long peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
conversationsService.pin(mapOf("peer_id" to peerId.toString())).mapApiDefault() convosService.pin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
} }
override suspend fun unpin( override suspend fun unpin(
peerId: Long peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
conversationsService.unpin(mapOf("peer_id" to peerId.toString())).mapApiDefault() convosService.unpin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
} }
override suspend fun reorderPinned( override suspend fun reorderPinned(
peerIds: List<Long> peerIds: List<Long>
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
conversationsService convosService
.reorderPinned(mapOf("peer_ids" to peerIds.joinToString(","))) .reorderPinned(mapOf("peer_ids" to peerIds.joinToString(",")))
.mapApiDefault() .mapApiDefault()
} }
@@ -190,12 +190,12 @@ class ConversationsRepositoryImpl(
override suspend fun archive( override suspend fun archive(
peerId: Long peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
conversationsService.archive(mapOf("peer_id" to peerId.toString())).mapApiDefault() convosService.archive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
} }
override suspend fun unarchive( override suspend fun unarchive(
peerId: Long peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
conversationsService.unarchive(mapOf("peer_id" to peerId.toString())).mapApiDefault() convosService.unarchive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
} }
} }
@@ -1,9 +1,9 @@
package dev.meloda.fast.data.api.messages package dev.meloda.fast.data.api.messages
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
data class MessagesHistoryInfo( data class MessagesHistoryInfo(
val messages: List<VkMessage>, val messages: List<VkMessage>,
val conversations: List<VkConversation> val convos: List<VkConvo>
) )
@@ -5,7 +5,7 @@ import dev.meloda.fast.model.api.data.VkChatData
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse import dev.meloda.fast.model.api.responses.MessagesGetConvoMembersResponse
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
import dev.meloda.fast.model.api.responses.MessagesSendResponse import dev.meloda.fast.model.api.responses.MessagesSendResponse
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
@@ -15,7 +15,7 @@ interface MessagesRepository {
suspend fun storeMessages(messages: List<VkMessage>) suspend fun storeMessages(messages: List<VkMessage>)
suspend fun getHistory( suspend fun getHistory(
conversationId: Long, convoId: Long,
offset: Int?, offset: Int?,
count: Int? count: Int?
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> ): ApiResult<MessagesHistoryInfo, RestApiErrorDomain>
@@ -99,13 +99,13 @@ interface MessagesRepository {
fields: String? = null fields: String? = null
): ApiResult<VkChatData, RestApiErrorDomain> ): ApiResult<VkChatData, RestApiErrorDomain>
suspend fun getConversationMembers( suspend fun getConvoMembers(
peerId: Long, peerId: Long,
offset: Int? = null, offset: Int? = null,
count: Int? = null, count: Int? = null,
extended: Boolean? = null, extended: Boolean? = null,
fields: String? = null fields: String? = null
): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain> ): ApiResult<MessagesGetConvoMembersResponse, RestApiErrorDomain>
suspend fun removeChatUser( suspend fun removeChatUser(
chatId: Long, chatId: Long,
@@ -5,7 +5,7 @@ import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.data.VkGroupsMap import dev.meloda.fast.data.VkGroupsMap
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.data.VkUsersMap import dev.meloda.fast.data.VkUsersMap
import dev.meloda.fast.database.dao.ConversationDao import dev.meloda.fast.database.dao.ConvoDao
import dev.meloda.fast.database.dao.GroupDao import dev.meloda.fast.database.dao.GroupDao
import dev.meloda.fast.database.dao.MessageDao import dev.meloda.fast.database.dao.MessageDao
import dev.meloda.fast.database.dao.UserDao import dev.meloda.fast.database.dao.UserDao
@@ -17,7 +17,7 @@ import dev.meloda.fast.model.api.data.VkUserData
import dev.meloda.fast.model.api.data.asDomain import dev.meloda.fast.model.api.data.asDomain
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkGroupDomain import dev.meloda.fast.model.api.domain.VkGroupDomain
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
@@ -27,7 +27,7 @@ import dev.meloda.fast.model.api.requests.MessagesDeleteRequest
import dev.meloda.fast.model.api.requests.MessagesEditRequest import dev.meloda.fast.model.api.requests.MessagesEditRequest
import dev.meloda.fast.model.api.requests.MessagesGetByIdRequest import dev.meloda.fast.model.api.requests.MessagesGetByIdRequest
import dev.meloda.fast.model.api.requests.MessagesGetChatRequest import dev.meloda.fast.model.api.requests.MessagesGetChatRequest
import dev.meloda.fast.model.api.requests.MessagesGetConversationMembersRequest import dev.meloda.fast.model.api.requests.MessagesGetConvoMembersRequest
import dev.meloda.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest import dev.meloda.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest
import dev.meloda.fast.model.api.requests.MessagesGetHistoryRequest import dev.meloda.fast.model.api.requests.MessagesGetHistoryRequest
import dev.meloda.fast.model.api.requests.MessagesMarkAsImportantRequest import dev.meloda.fast.model.api.requests.MessagesMarkAsImportantRequest
@@ -36,7 +36,7 @@ import dev.meloda.fast.model.api.requests.MessagesPinMessageRequest
import dev.meloda.fast.model.api.requests.MessagesRemoveChatUserRequest import dev.meloda.fast.model.api.requests.MessagesRemoveChatUserRequest
import dev.meloda.fast.model.api.requests.MessagesSendRequest import dev.meloda.fast.model.api.requests.MessagesSendRequest
import dev.meloda.fast.model.api.requests.MessagesUnpinMessageRequest import dev.meloda.fast.model.api.requests.MessagesUnpinMessageRequest
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse import dev.meloda.fast.model.api.responses.MessagesGetConvoMembersResponse
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
import dev.meloda.fast.model.api.responses.MessagesSendResponse import dev.meloda.fast.model.api.responses.MessagesSendResponse
import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.RestApiErrorDomain
@@ -52,18 +52,18 @@ class MessagesRepositoryImpl(
private val messageDao: MessageDao, private val messageDao: MessageDao,
private val userDao: UserDao, private val userDao: UserDao,
private val groupDao: GroupDao, private val groupDao: GroupDao,
private val conversationDao: ConversationDao private val convoDao: ConvoDao
) : MessagesRepository { ) : MessagesRepository {
override suspend fun getHistory( override suspend fun getHistory(
conversationId: Long, convoId: Long,
offset: Int?, offset: Int?,
count: Int? count: Int?
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) { ): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesGetHistoryRequest( val requestModel = MessagesGetHistoryRequest(
count = count, count = count,
offset = offset, offset = offset,
peerId = conversationId, peerId = convoId,
extended = true, extended = true,
startMessageId = null, startMessageId = null,
rev = null, rev = null,
@@ -104,19 +104,19 @@ class MessagesRepositoryImpl(
} }
} }
val conversations = response.conversations.orEmpty().map { item -> val convos = response.convos.orEmpty().map { item ->
val message = messages.firstOrNull { it.id == item.lastMessageId } val message = messages.firstOrNull { it.id == item.lastMessageId }
item.asDomain(message) item.asDomain(message)
.let { conversation -> .let { convo ->
conversation.copy( convo.copy(
user = usersMap.conversationUser(conversation), user = usersMap.convoUser(convo),
group = groupsMap.conversationGroup(conversation) group = groupsMap.convoGroup(convo)
).also { VkMemoryCache[conversation.id] = it } ).also { VkMemoryCache[convo.id] = it }
} }
} }
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity)) convoDao.insertAll(convos.map(VkConvo::asEntity))
messageDao.insertAll(messages.map(VkMessage::asEntity)) messageDao.insertAll(messages.map(VkMessage::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity)) userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity)) groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
@@ -124,7 +124,7 @@ class MessagesRepositoryImpl(
MessagesHistoryInfo( MessagesHistoryInfo(
messages = messages, messages = messages,
conversations = conversations convos = convos
) )
}, },
errorMapper = { error -> errorMapper = { error ->
@@ -243,7 +243,7 @@ class MessagesRepositoryImpl(
offset = offset, offset = offset,
preserveOrder = true, preserveOrder = true,
attachmentTypes = attachmentTypes, attachmentTypes = attachmentTypes,
conversationMessageId = cmId, cmId = cmId,
fields = VkConstants.ALL_FIELDS fields = VkConstants.ALL_FIELDS
) )
@@ -297,7 +297,7 @@ class MessagesRepositoryImpl(
val requestModel = MessagesPinMessageRequest( val requestModel = MessagesPinMessageRequest(
peerId = peerId, peerId = peerId,
messageId = messageId, messageId = messageId,
conversationMessageId = cmId cmId = cmId
) )
messagesService.pin(requestModel.map).mapApiResult( messagesService.pin(requestModel.map).mapApiResult(
@@ -343,7 +343,7 @@ class MessagesRepositoryImpl(
val requestModel = MessagesDeleteRequest( val requestModel = MessagesDeleteRequest(
peerId = peerId, peerId = peerId,
messagesIds = messageIds, messagesIds = messageIds,
conversationsMessagesIds = cmIds, cmIds = cmIds,
isSpam = spam, isSpam = spam,
deleteForAll = deleteForAll deleteForAll = deleteForAll
) )
@@ -394,15 +394,15 @@ class MessagesRepositoryImpl(
messagesService.getChat(requestModel.map).mapApiDefault() messagesService.getChat(requestModel.map).mapApiDefault()
} }
override suspend fun getConversationMembers( override suspend fun getConvoMembers(
peerId: Long, peerId: Long,
offset: Int?, offset: Int?,
count: Int?, count: Int?,
extended: Boolean?, extended: Boolean?,
fields: String? fields: String?
): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain> = ): ApiResult<MessagesGetConvoMembersResponse, RestApiErrorDomain> =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val requestModel = MessagesGetConversationMembersRequest( val requestModel = MessagesGetConvoMembersRequest(
peerId = peerId, peerId = peerId,
offset = offset, offset = offset,
count = count, count = count,
@@ -410,7 +410,7 @@ class MessagesRepositoryImpl(
fields = fields fields = fields
) )
messagesService.getConversationMembers(requestModel.map).mapApiDefault() messagesService.getConvoMembers(requestModel.map).mapApiDefault()
} }
override suspend fun removeChatUser( override suspend fun removeChatUser(
@@ -6,8 +6,8 @@ import dev.meloda.fast.data.api.account.AccountRepositoryImpl
import dev.meloda.fast.data.api.audios.AudiosRepository import dev.meloda.fast.data.api.audios.AudiosRepository
import dev.meloda.fast.data.api.auth.AuthRepository import dev.meloda.fast.data.api.auth.AuthRepository
import dev.meloda.fast.data.api.auth.AuthRepositoryImpl import dev.meloda.fast.data.api.auth.AuthRepositoryImpl
import dev.meloda.fast.data.api.conversations.ConversationsRepository import dev.meloda.fast.data.api.convos.ConvosRepository
import dev.meloda.fast.data.api.conversations.ConversationsRepositoryImpl import dev.meloda.fast.data.api.convos.ConvosRepositoryImpl
import dev.meloda.fast.data.api.files.FilesRepository import dev.meloda.fast.data.api.files.FilesRepository
import dev.meloda.fast.data.api.friends.FriendsRepository import dev.meloda.fast.data.api.friends.FriendsRepository
import dev.meloda.fast.data.api.friends.FriendsRepositoryImpl import dev.meloda.fast.data.api.friends.FriendsRepositoryImpl
@@ -45,7 +45,7 @@ val dataModule = module {
singleOf(::AuthRepositoryImpl) bind AuthRepository::class singleOf(::AuthRepositoryImpl) bind AuthRepository::class
singleOf(::ConversationsRepositoryImpl) bind ConversationsRepository::class singleOf(::ConvosRepositoryImpl) bind ConvosRepository::class
singleOf(::FilesRepository) singleOf(::FilesRepository)
@@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 10, "version": 10,
"identityHash": "fa307a5eb2e1f7d601bd1374174635cd", "identityHash": "6c315b7f800694f635318d86032746ec",
"entities": [ "entities": [
{ {
"tableName": "users", "tableName": "users",
@@ -41,50 +41,42 @@
{ {
"fieldPath": "onlineAppId", "fieldPath": "onlineAppId",
"columnName": "onlineAppId", "columnName": "onlineAppId",
"affinity": "INTEGER", "affinity": "INTEGER"
"notNull": false
}, },
{ {
"fieldPath": "lastSeen", "fieldPath": "lastSeen",
"columnName": "lastSeen", "columnName": "lastSeen",
"affinity": "INTEGER", "affinity": "INTEGER"
"notNull": false
}, },
{ {
"fieldPath": "lastSeenStatus", "fieldPath": "lastSeenStatus",
"columnName": "lastSeenStatus", "columnName": "lastSeenStatus",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "birthday", "fieldPath": "birthday",
"columnName": "birthday", "columnName": "birthday",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "photo50", "fieldPath": "photo50",
"columnName": "photo50", "columnName": "photo50",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "photo100", "fieldPath": "photo100",
"columnName": "photo100", "columnName": "photo100",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "photo200", "fieldPath": "photo200",
"columnName": "photo200", "columnName": "photo200",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "photo400Orig", "fieldPath": "photo400Orig",
"columnName": "photo400Orig", "columnName": "photo400Orig",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
} }
], ],
"primaryKey": { "primaryKey": {
@@ -92,9 +84,7 @@
"columnNames": [ "columnNames": [
"id" "id"
] ]
}, }
"indices": [],
"foreignKeys": []
}, },
{ {
"tableName": "groups", "tableName": "groups",
@@ -121,26 +111,22 @@
{ {
"fieldPath": "photo50", "fieldPath": "photo50",
"columnName": "photo50", "columnName": "photo50",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "photo100", "fieldPath": "photo100",
"columnName": "photo100", "columnName": "photo100",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "photo200", "fieldPath": "photo200",
"columnName": "photo200", "columnName": "photo200",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "membersCount", "fieldPath": "membersCount",
"columnName": "membersCount", "columnName": "membersCount",
"affinity": "INTEGER", "affinity": "INTEGER"
"notNull": false
} }
], ],
"primaryKey": { "primaryKey": {
@@ -148,13 +134,11 @@
"columnNames": [ "columnNames": [
"id" "id"
] ]
}, }
"indices": [],
"foreignKeys": []
}, },
{ {
"tableName": "messages", "tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `conversationMessageId` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionConversationMessageId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, `pinnedAt` INTEGER, `isPinned` INTEGER NOT NULL, PRIMARY KEY(`id`))", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `cmId` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionCmId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, `pinnedAt` INTEGER, `isPinned` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@@ -163,16 +147,15 @@
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "conversationMessageId", "fieldPath": "cmId",
"columnName": "conversationMessageId", "columnName": "cmId",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "text", "fieldPath": "text",
"columnName": "text", "columnName": "text",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "isOut", "fieldPath": "isOut",
@@ -207,38 +190,32 @@
{ {
"fieldPath": "action", "fieldPath": "action",
"columnName": "action", "columnName": "action",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "actionMemberId", "fieldPath": "actionMemberId",
"columnName": "actionMemberId", "columnName": "actionMemberId",
"affinity": "INTEGER", "affinity": "INTEGER"
"notNull": false
}, },
{ {
"fieldPath": "actionText", "fieldPath": "actionText",
"columnName": "actionText", "columnName": "actionText",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "actionConversationMessageId", "fieldPath": "actionCmId",
"columnName": "actionConversationMessageId", "columnName": "actionCmId",
"affinity": "INTEGER", "affinity": "INTEGER"
"notNull": false
}, },
{ {
"fieldPath": "actionMessage", "fieldPath": "actionMessage",
"columnName": "actionMessage", "columnName": "actionMessage",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "updateTime", "fieldPath": "updateTime",
"columnName": "updateTime", "columnName": "updateTime",
"affinity": "INTEGER", "affinity": "INTEGER"
"notNull": false
}, },
{ {
"fieldPath": "important", "fieldPath": "important",
@@ -249,32 +226,27 @@
{ {
"fieldPath": "forwardIds", "fieldPath": "forwardIds",
"columnName": "forwardIds", "columnName": "forwardIds",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "attachments", "fieldPath": "attachments",
"columnName": "attachments", "columnName": "attachments",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "replyMessageId", "fieldPath": "replyMessageId",
"columnName": "replyMessageId", "columnName": "replyMessageId",
"affinity": "INTEGER", "affinity": "INTEGER"
"notNull": false
}, },
{ {
"fieldPath": "geoType", "fieldPath": "geoType",
"columnName": "geoType", "columnName": "geoType",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "pinnedAt", "fieldPath": "pinnedAt",
"columnName": "pinnedAt", "columnName": "pinnedAt",
"affinity": "INTEGER", "affinity": "INTEGER"
"notNull": false
}, },
{ {
"fieldPath": "isPinned", "fieldPath": "isPinned",
@@ -288,13 +260,11 @@
"columnNames": [ "columnNames": [
"id" "id"
] ]
}, }
"indices": [],
"foreignKeys": []
}, },
{ {
"tableName": "conversations", "tableName": "conversations",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastConversationMessageId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, `isArchived` INTEGER NOT NULL, PRIMARY KEY(`id`))", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastCmId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, `isArchived` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@@ -311,32 +281,27 @@
{ {
"fieldPath": "ownerId", "fieldPath": "ownerId",
"columnName": "ownerId", "columnName": "ownerId",
"affinity": "INTEGER", "affinity": "INTEGER"
"notNull": false
}, },
{ {
"fieldPath": "title", "fieldPath": "title",
"columnName": "title", "columnName": "title",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "photo50", "fieldPath": "photo50",
"columnName": "photo50", "columnName": "photo50",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "photo100", "fieldPath": "photo100",
"columnName": "photo100", "columnName": "photo100",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "photo200", "fieldPath": "photo200",
"columnName": "photo200", "columnName": "photo200",
"affinity": "TEXT", "affinity": "TEXT"
"notNull": false
}, },
{ {
"fieldPath": "isPhantom", "fieldPath": "isPhantom",
@@ -345,8 +310,8 @@
"notNull": true "notNull": true
}, },
{ {
"fieldPath": "lastConversationMessageId", "fieldPath": "lastCmId",
"columnName": "lastConversationMessageId", "columnName": "lastCmId",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": true
}, },
@@ -377,8 +342,7 @@
{ {
"fieldPath": "lastMessageId", "fieldPath": "lastMessageId",
"columnName": "lastMessageId", "columnName": "lastMessageId",
"affinity": "INTEGER", "affinity": "INTEGER"
"notNull": false
}, },
{ {
"fieldPath": "unreadCount", "fieldPath": "unreadCount",
@@ -389,8 +353,7 @@
{ {
"fieldPath": "membersCount", "fieldPath": "membersCount",
"columnName": "membersCount", "columnName": "membersCount",
"affinity": "INTEGER", "affinity": "INTEGER"
"notNull": false
}, },
{ {
"fieldPath": "canChangePin", "fieldPath": "canChangePin",
@@ -419,8 +382,7 @@
{ {
"fieldPath": "pinnedMessageId", "fieldPath": "pinnedMessageId",
"columnName": "pinnedMessageId", "columnName": "pinnedMessageId",
"affinity": "INTEGER", "affinity": "INTEGER"
"notNull": false
}, },
{ {
"fieldPath": "peerType", "fieldPath": "peerType",
@@ -440,15 +402,12 @@
"columnNames": [ "columnNames": [
"id" "id"
] ]
}, }
"indices": [],
"foreignKeys": []
} }
], ],
"views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fa307a5eb2e1f7d601bd1374174635cd')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6c315b7f800694f635318d86032746ec')"
] ]
} }
} }
@@ -0,0 +1,413 @@
{
"formatVersion": 1,
"database": {
"version": 11,
"identityHash": "a746865995959331f8a1b512c049dacb",
"entities": [
{
"tableName": "users",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `isOnline` INTEGER NOT NULL, `isOnlineMobile` INTEGER NOT NULL, `onlineAppId` INTEGER, `lastSeen` INTEGER, `lastSeenStatus` TEXT, `birthday` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `photo400Orig` TEXT, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "firstName",
"columnName": "firstName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "lastName",
"columnName": "lastName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isOnline",
"columnName": "isOnline",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isOnlineMobile",
"columnName": "isOnlineMobile",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "onlineAppId",
"columnName": "onlineAppId",
"affinity": "INTEGER"
},
{
"fieldPath": "lastSeen",
"columnName": "lastSeen",
"affinity": "INTEGER"
},
{
"fieldPath": "lastSeenStatus",
"columnName": "lastSeenStatus",
"affinity": "TEXT"
},
{
"fieldPath": "birthday",
"columnName": "birthday",
"affinity": "TEXT"
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "photo400Orig",
"columnName": "photo400Orig",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "groups",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "screenName",
"columnName": "screenName",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER"
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "messages",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `cmId` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionCmId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, `pinnedAt` INTEGER, `isPinned` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "cmId",
"columnName": "cmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT"
},
{
"fieldPath": "isOut",
"columnName": "isOut",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "peerId",
"columnName": "peerId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fromId",
"columnName": "fromId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "date",
"columnName": "date",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "randomId",
"columnName": "randomId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "action",
"columnName": "action",
"affinity": "TEXT"
},
{
"fieldPath": "actionMemberId",
"columnName": "actionMemberId",
"affinity": "INTEGER"
},
{
"fieldPath": "actionText",
"columnName": "actionText",
"affinity": "TEXT"
},
{
"fieldPath": "actionCmId",
"columnName": "actionCmId",
"affinity": "INTEGER"
},
{
"fieldPath": "actionMessage",
"columnName": "actionMessage",
"affinity": "TEXT"
},
{
"fieldPath": "updateTime",
"columnName": "updateTime",
"affinity": "INTEGER"
},
{
"fieldPath": "important",
"columnName": "important",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "forwardIds",
"columnName": "forwardIds",
"affinity": "TEXT"
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT"
},
{
"fieldPath": "replyMessageId",
"columnName": "replyMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "geoType",
"columnName": "geoType",
"affinity": "TEXT"
},
{
"fieldPath": "pinnedAt",
"columnName": "pinnedAt",
"affinity": "INTEGER"
},
{
"fieldPath": "isPinned",
"columnName": "isPinned",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
},
{
"tableName": "convos",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastCmId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, `isArchived` INTEGER NOT NULL, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "localId",
"columnName": "localId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "INTEGER"
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT"
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT"
},
{
"fieldPath": "isPhantom",
"columnName": "isPhantom",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastCmId",
"columnName": "lastCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inReadCmId",
"columnName": "inReadCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outReadCmId",
"columnName": "outReadCmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "inRead",
"columnName": "inRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "outRead",
"columnName": "outRead",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "lastMessageId",
"columnName": "lastMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "unreadCount",
"columnName": "unreadCount",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER"
},
{
"fieldPath": "canChangePin",
"columnName": "canChangePin",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "canChangeInfo",
"columnName": "canChangeInfo",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "majorId",
"columnName": "majorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "minorId",
"columnName": "minorId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "pinnedMessageId",
"columnName": "pinnedMessageId",
"affinity": "INTEGER"
},
{
"fieldPath": "peerType",
"columnName": "peerType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "isArchived",
"columnName": "isArchived",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
}
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a746865995959331f8a1b512c049dacb')"
]
}
}
@@ -3,12 +3,12 @@ package dev.meloda.fast.database
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters import androidx.room.TypeConverters
import dev.meloda.fast.database.dao.ConversationDao import dev.meloda.fast.database.dao.ConvoDao
import dev.meloda.fast.database.dao.GroupDao import dev.meloda.fast.database.dao.GroupDao
import dev.meloda.fast.database.dao.MessageDao import dev.meloda.fast.database.dao.MessageDao
import dev.meloda.fast.database.dao.UserDao import dev.meloda.fast.database.dao.UserDao
import dev.meloda.fast.database.typeconverters.Converters import dev.meloda.fast.database.typeconverters.Converters
import dev.meloda.fast.model.database.VkConversationEntity import dev.meloda.fast.model.database.VkConvoEntity
import dev.meloda.fast.model.database.VkGroupEntity import dev.meloda.fast.model.database.VkGroupEntity
import dev.meloda.fast.model.database.VkMessageEntity import dev.meloda.fast.model.database.VkMessageEntity
import dev.meloda.fast.model.database.VkUserEntity import dev.meloda.fast.model.database.VkUserEntity
@@ -18,15 +18,15 @@ import dev.meloda.fast.model.database.VkUserEntity
VkUserEntity::class, VkUserEntity::class,
VkGroupEntity::class, VkGroupEntity::class,
VkMessageEntity::class, VkMessageEntity::class,
VkConversationEntity::class VkConvoEntity::class
], ],
version = 10 version = 11
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class CacheDatabase : RoomDatabase() { abstract class CacheDatabase : RoomDatabase() {
abstract fun userDao(): UserDao abstract fun userDao(): UserDao
abstract fun groupDao(): GroupDao abstract fun groupDao(): GroupDao
abstract fun messageDao(): MessageDao abstract fun messageDao(): MessageDao
abstract fun conversationDao(): ConversationDao abstract fun convoDao(): ConvoDao
} }
@@ -1,30 +0,0 @@
package dev.meloda.fast.database.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import dev.meloda.fast.model.database.ConversationWithMessage
import dev.meloda.fast.model.database.VkConversationEntity
@Dao
abstract class ConversationDao : EntityDao<VkConversationEntity> {
@Query("SELECT * FROM conversations")
abstract suspend fun getAll(): List<VkConversationEntity>
@Query("SELECT * FROM conversations WHERE id IN (:ids)")
abstract suspend fun getAllByIds(ids: List<Int>): List<VkConversationEntity>
@Query("SELECT * FROM conversations WHERE id IS (:id)")
abstract suspend fun getById(id: Long): VkConversationEntity?
@Transaction
@Query("SELECT * FROM conversations WHERE id IS (:id)")
abstract suspend fun getByIdWithMessage(id: Long): ConversationWithMessage?
@Query("DELETE FROM conversations WHERE rowid IN (:ids)")
abstract suspend fun deleteByIds(ids: List<Int>): Int
}
@@ -0,0 +1,30 @@
package dev.meloda.fast.database.dao
import androidx.room.Dao
import androidx.room.Query
import androidx.room.Transaction
import dev.meloda.fast.model.database.ConvoWithMessage
import dev.meloda.fast.model.database.VkConvoEntity
@Dao
abstract class ConvoDao : EntityDao<VkConvoEntity> {
@Query("SELECT * FROM convos")
abstract suspend fun getAll(): List<VkConvoEntity>
@Query("SELECT * FROM convos WHERE id IN (:ids)")
abstract suspend fun getAllByIds(ids: List<Int>): List<VkConvoEntity>
@Query("SELECT * FROM convos WHERE id IS (:id)")
abstract suspend fun getById(id: Long): VkConvoEntity?
@Transaction
@Query("SELECT * FROM convos WHERE id IS (:id)")
abstract suspend fun getByIdWithMessage(id: Long): ConvoWithMessage?
@Query("DELETE FROM convos WHERE rowid IN (:ids)")
abstract suspend fun deleteByIds(ids: List<Int>): Int
}
@@ -10,8 +10,8 @@ abstract class MessageDao : EntityDao<VkMessageEntity> {
@Query("SELECT * FROM messages") @Query("SELECT * FROM messages")
abstract suspend fun getAll(): List<VkMessageEntity> abstract suspend fun getAll(): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE peerId IS (:conversationId)") @Query("SELECT * FROM messages WHERE peerId IS (:convoId)")
abstract suspend fun getAll(conversationId: Long): List<VkMessageEntity> abstract suspend fun getAll(convoId: Long): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE id IN (:ids)") @Query("SELECT * FROM messages WHERE id IN (:ids)")
abstract suspend fun getAllByIds(ids: List<Int>): List<VkMessageEntity> abstract suspend fun getAllByIds(ids: List<Int>): List<VkMessageEntity>
@@ -23,7 +23,7 @@ val databaseModule = module {
single { cacheDB().userDao() } single { cacheDB().userDao() }
single { cacheDB().groupDao() } single { cacheDB().groupDao() }
single { cacheDB().messageDao() } single { cacheDB().messageDao() }
single { cacheDB().conversationDao() } single { cacheDB().convoDao() }
} }
private fun Scope.cacheDB(): CacheDatabase = get() private fun Scope.cacheDB(): CacheDatabase = get()
+5
View File
@@ -8,6 +8,7 @@ android {
} }
dependencies { dependencies {
api(projects.core.common)
api(projects.core.data) api(projects.core.data)
api(projects.core.model) api(projects.core.model)
@@ -15,4 +16,8 @@ dependencies {
implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.core)
implementation(libs.koin.core) implementation(libs.koin.core)
implementation(libs.eithernet) implementation(libs.eithernet)
implementation(libs.bundles.nanokt)
implementation(libs.compose.ui)
} }
@@ -1,25 +1,25 @@
package dev.meloda.fast.domain package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.model.ConversationsFilter import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface ConversationsUseCase : BaseUseCase { interface ConvoUseCase : BaseUseCase {
suspend fun storeConversations(conversations: List<VkConversation>) suspend fun storeConvos(convos: List<VkConvo>)
fun getConversations( fun getConvos(
count: Int? = null, count: Int? = null,
offset: Int? = null, offset: Int? = null,
filter: ConversationsFilter filter: ConvosFilter
): Flow<State<List<VkConversation>>> ): Flow<State<List<VkConvo>>>
fun getById( fun getById(
peerIds: List<Long>, peerIds: List<Long>,
extended: Boolean? = null, extended: Boolean? = null,
fields: String? = null fields: String? = null
): Flow<State<List<VkConversation>>> ): Flow<State<List<VkConvo>>>
fun delete(peerId: Long): Flow<State<Long>> fun delete(peerId: Long): Flow<State<Long>>
@@ -1,30 +1,30 @@
package dev.meloda.fast.domain package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.conversations.ConversationsRepository import dev.meloda.fast.data.api.convos.ConvosRepository
import dev.meloda.fast.data.mapToState import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.ConversationsFilter import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
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
class ConversationsUseCaseImpl( class ConvoUseCaseImpl(
private val repository: ConversationsRepository, private val repository: ConvosRepository,
) : ConversationsUseCase { ) : ConvoUseCase {
override suspend fun storeConversations( override suspend fun storeConvos(
conversations: List<VkConversation> convos: List<VkConvo>
) = withContext(Dispatchers.IO) { ) = withContext(Dispatchers.IO) {
repository.storeConversations(conversations) repository.storeConvos(convos)
} }
override fun getConversations( override fun getConvos(
count: Int?, count: Int?,
offset: Int?, offset: Int?,
filter: ConversationsFilter filter: ConvosFilter
): Flow<State<List<VkConversation>>> = flowNewState { ): Flow<State<List<VkConvo>>> = flowNewState {
repository.getConversations( repository.getConvos(
count = count, count = count,
offset = offset, offset = offset,
filter = filter filter = filter
@@ -35,8 +35,8 @@ class ConversationsUseCaseImpl(
peerIds: List<Long>, peerIds: List<Long>,
extended: Boolean?, extended: Boolean?,
fields: String? fields: String?
): Flow<State<List<VkConversation>>> = flowNewState { ): Flow<State<List<VkConvo>>> = flowNewState {
repository.getConversationsById( repository.getConvosById(
peerIds = peerIds, peerIds = peerIds,
extended = extended, extended = extended,
fields = fields fields = fields
@@ -1,22 +1,22 @@
package dev.meloda.fast.domain package dev.meloda.fast.domain
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.conversations.ConversationsRepository import dev.meloda.fast.data.api.convos.ConvosRepository
import dev.meloda.fast.data.mapToState import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class LoadConversationsByIdUseCase( class LoadConvosByIdUseCase(
private val conversationsRepository: ConversationsRepository private val convosRepository: ConvosRepository
) : BaseUseCase { ) : BaseUseCase {
operator fun invoke( operator fun invoke(
peerIds: List<Long>, peerIds: List<Long>,
extended: Boolean? = null, extended: Boolean? = null,
fields: String? = null fields: String? = null
): Flow<State<List<VkConversation>>> = flowNewState { ): Flow<State<List<VkConvo>>> = flowNewState {
conversationsRepository convosRepository
.getConversationsById( .getConvosById(
peerIds = peerIds, peerIds = peerIds,
extended = extended, extended = extended,
fields = fields, fields = fields,
@@ -9,12 +9,12 @@ import dev.meloda.fast.common.extensions.toList
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.processState import dev.meloda.fast.data.processState
import dev.meloda.fast.model.ApiEvent import dev.meloda.fast.model.ApiEvent
import dev.meloda.fast.model.ConversationFlags import dev.meloda.fast.model.ConvoFlags
import dev.meloda.fast.model.InteractionType import dev.meloda.fast.model.InteractionType
import dev.meloda.fast.model.LongPollEvent import dev.meloda.fast.model.LongPollEvent
import dev.meloda.fast.model.LongPollParsedEvent import dev.meloda.fast.model.LongPollParsedEvent
import dev.meloda.fast.model.MessageFlags import dev.meloda.fast.model.MessageFlags
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -28,7 +28,7 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
class LongPollUpdatesParser( class LongPollUpdatesParser(
private val conversationsUseCase: ConversationsUseCase, private val convoUseCase: ConvoUseCase,
private val messagesUseCase: MessagesUseCase private val messagesUseCase: MessagesUseCase
) { ) {
private val job = SupervisorJob() private val job = SupervisorJob()
@@ -271,9 +271,9 @@ class LongPollUpdatesParser(
val message = val message =
async { loadMessage(peerId = peerId, cmId = cmId) }.await() async { loadMessage(peerId = peerId, cmId = cmId) }.await()
val conversation = val convo =
async { async {
loadConversation( loadConvo(
peerId = peerId, peerId = peerId,
extended = true, extended = true,
fields = VkConstants.ALL_FIELDS fields = VkConstants.ALL_FIELDS
@@ -287,7 +287,7 @@ class LongPollUpdatesParser(
.onEvent( .onEvent(
LongPollParsedEvent.NewMessage( LongPollParsedEvent.NewMessage(
message = message, message = message,
inArchive = conversation?.isArchived == true inArchive = convo?.isArchived == true
// TODO: 03-Apr-25, Danil Nikolaev: // TODO: 03-Apr-25, Danil Nikolaev:
// load user settings about restoring chats with // load user settings about restoring chats with
// enabled notifications from archive // enabled notifications from archive
@@ -368,13 +368,13 @@ class LongPollUpdatesParser(
val eventsToSend = mutableListOf<LongPollParsedEvent>() val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = ConversationFlags.parse(flags) val parsedFlags = ConvoFlags.parse(flags)
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
parsedFlags.forEach { flag -> parsedFlags.forEach { flag ->
when (flag) { when (flag) {
ConversationFlags.ARCHIVED -> { ConvoFlags.ARCHIVED -> {
val conversation = loadConversation( val convo = loadConvo(
peerId = peerId, peerId = peerId,
extended = true, extended = true,
fields = VkConstants.ALL_FIELDS fields = VkConstants.ALL_FIELDS
@@ -382,11 +382,11 @@ class LongPollUpdatesParser(
val message = loadMessage( val message = loadMessage(
peerId = peerId, peerId = peerId,
cmId = conversation.lastCmId cmId = convo.lastCmId
) )
val eventToSend = LongPollParsedEvent.ChatArchived( val eventToSend = LongPollParsedEvent.ChatArchived(
conversation = conversation.copy(lastMessage = message), convo = convo.copy(lastMessage = message),
archived = false archived = false
) )
eventsToSend += eventToSend eventsToSend += eventToSend
@@ -423,13 +423,13 @@ class LongPollUpdatesParser(
val eventsToSend = mutableListOf<LongPollParsedEvent>() val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = ConversationFlags.parse(flags) val parsedFlags = ConvoFlags.parse(flags)
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
parsedFlags.forEach { flag -> parsedFlags.forEach { flag ->
when (flag) { when (flag) {
ConversationFlags.ARCHIVED -> { ConvoFlags.ARCHIVED -> {
val conversation = loadConversation( val convo = loadConvo(
peerId = peerId, peerId = peerId,
extended = true, extended = true,
fields = VkConstants.ALL_FIELDS fields = VkConstants.ALL_FIELDS
@@ -437,11 +437,11 @@ class LongPollUpdatesParser(
val message = loadMessage( val message = loadMessage(
peerId = peerId, peerId = peerId,
cmId = conversation.lastCmId cmId = convo.lastCmId
) )
val eventToSend = LongPollParsedEvent.ChatArchived( val eventToSend = LongPollParsedEvent.ChatArchived(
conversation = conversation.copy(lastMessage = message), convo = convo.copy(lastMessage = message),
archived = true archived = true
) )
eventsToSend += eventToSend eventsToSend += eventToSend
@@ -673,29 +673,29 @@ class LongPollUpdatesParser(
} }
} }
private suspend fun loadConversation( private suspend fun loadConvo(
peerId: Long, peerId: Long,
extended: Boolean = false, extended: Boolean = false,
fields: String? = null fields: String? = null
): VkConversation? = suspendCoroutine { continuation -> ): VkConvo? = suspendCoroutine { continuation ->
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
conversationsUseCase.getById( convoUseCase.getById(
peerIds = listOf(peerId), peerIds = listOf(peerId),
extended = extended, extended = extended,
fields = fields fields = fields
).listenValue(coroutineScope) { state -> ).listenValue(coroutineScope) { state ->
state.processState( state.processState(
error = { error -> error = { error ->
Log.e("LongPollUpdatesParser", "loadConversation: error: $error") Log.e("LongPollUpdatesParser", "loadConvo: error: $error")
continuation.resume(null) continuation.resume(null)
}, },
success = { response -> success = { response ->
val conversation = response.singleOrNull() ?: run { val convo = response.singleOrNull() ?: run {
continuation.resume(null) continuation.resume(null)
return@listenValue return@listenValue
} }
continuation.resume(conversation) continuation.resume(convo)
} }
) )
} }
@@ -14,7 +14,7 @@ interface MessagesUseCase : BaseUseCase {
suspend fun storeMessages(messages: List<VkMessage>) suspend fun storeMessages(messages: List<VkMessage>)
fun getMessagesHistory( fun getMessagesHistory(
conversationId: Long, convoId: Long,
count: Int?, count: Int?,
offset: Int? offset: Int?
): Flow<State<MessagesHistoryInfo>> ): Flow<State<MessagesHistoryInfo>>
@@ -23,12 +23,12 @@ class MessagesUseCaseImpl(
} }
override fun getMessagesHistory( override fun getMessagesHistory(
conversationId: Long, convoId: Long,
count: Int?, count: Int?,
offset: Int? offset: Int?
): Flow<State<MessagesHistoryInfo>> = flowNewState { ): Flow<State<MessagesHistoryInfo>> = flowNewState {
repository.getHistory( repository.getHistory(
conversationId = conversationId, convoId = convoId,
offset = offset, offset = offset,
count = count count = count
).mapToState() ).mapToState()
@@ -7,7 +7,7 @@ import dev.meloda.fast.domain.GetCurrentAccountUseCase
import dev.meloda.fast.domain.GetLocalUserByIdUseCase import dev.meloda.fast.domain.GetLocalUserByIdUseCase
import dev.meloda.fast.domain.GetLocalUsersByIdsUseCase import dev.meloda.fast.domain.GetLocalUsersByIdsUseCase
import dev.meloda.fast.domain.GetMessageReadPeersUseCase import dev.meloda.fast.domain.GetMessageReadPeersUseCase
import dev.meloda.fast.domain.LoadConversationsByIdUseCase import dev.meloda.fast.domain.LoadConvosByIdUseCase
import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.domain.LoadUsersByIdsUseCase import dev.meloda.fast.domain.LoadUsersByIdsUseCase
import dev.meloda.fast.domain.StoreUsersUseCase import dev.meloda.fast.domain.StoreUsersUseCase
@@ -27,7 +27,7 @@ val domainModule = module {
singleOf(::AccountUseCaseImpl) bind AccountUseCase::class singleOf(::AccountUseCaseImpl) bind AccountUseCase::class
singleOf(::GetCurrentAccountUseCase) singleOf(::GetCurrentAccountUseCase)
singleOf(::LoadConversationsByIdUseCase) singleOf(::LoadConvosByIdUseCase)
singleOf(::GetMessageReadPeersUseCase) singleOf(::GetMessageReadPeersUseCase)
} }
@@ -1,7 +1,6 @@
package dev.meloda.fast.conversations.util package dev.meloda.fast.domain.util
import android.content.res.Resources import android.content.res.Resources
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
@@ -13,64 +12,22 @@ import dev.meloda.fast.common.extensions.orDots
import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.common.model.UiText import dev.meloda.fast.common.model.UiText
import dev.meloda.fast.common.model.parseString import dev.meloda.fast.common.model.parseString
import dev.meloda.fast.common.util.TimeUtils
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.model.InteractionType import dev.meloda.fast.model.InteractionType
import dev.meloda.fast.model.api.PeerType import dev.meloda.fast.model.api.PeerType
import dev.meloda.fast.model.api.data.AttachmentType import dev.meloda.fast.model.api.data.AttachmentType
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.domain.VkVideoDomain import dev.meloda.fast.model.api.domain.VkVideoDomain
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.model.api.ActionState
import dev.meloda.fast.ui.model.api.ConversationOption
import dev.meloda.fast.ui.model.api.UiConversation
import dev.meloda.fast.ui.util.ImmutableList
import dev.meloda.fast.ui.util.emptyImmutableList
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
import kotlin.math.ln import kotlin.math.ln
import kotlin.math.pow import kotlin.math.pow
fun VkConversation.asPresentation( fun VkConvo.extractAvatar(): UiImage = when (peerType) {
resources: Resources,
useContactName: Boolean,
isExpanded: Boolean = false,
options: ImmutableList<ConversationOption> = emptyImmutableList()
): UiConversation = UiConversation(
id = id,
lastMessageId = lastMessageId,
avatar = extractAvatar(),
title = extractTitle(this, useContactName, resources),
unreadCount = extractUnreadCount(lastMessage, this),
date = TimeUtils.getLocalizedTime(
date = (lastMessage?.date ?: -1) * 1000L,
yearShort = { resources.getString(R.string.year_short) },
monthShort = { resources.getString(R.string.month_short) },
weekShort = { resources.getString(R.string.week_short) },
dayShort = { resources.getString(R.string.day_short) },
now = { resources.getString(R.string.time_now) },
),
message = extractMessage(resources, lastMessage, id, peerType),
attachmentImage = if (lastMessage?.text == null) null
else getAttachmentConversationIcon(lastMessage),
isPinned = majorId > 0,
actionImageId = ActionState.parse(isPhantom, isCallInProgress).getResourceId(),
isBirthday = extractBirthday(this),
isUnread = !isRead(),
isAccount = isAccount(id),
isOnline = !isAccount(id) && user?.onlineStatus?.isOnline() == true,
lastMessage = lastMessage,
peerType = peerType,
interactionText = extractInteractionText(resources, this),
isExpanded = isExpanded,
isArchived = isArchived,
options = options
)
fun VkConversation.extractAvatar() = when (peerType) {
PeerType.USER -> { PeerType.USER -> {
if (isAccount(id)) null if (isAccount(id)) null
else user?.photo200 else user?.photo200
@@ -85,18 +42,17 @@ fun VkConversation.extractAvatar() = when (peerType) {
} }
}?.let(UiImage::Url) ?: UiImage.Resource(R.drawable.ic_account_circle_cut) }?.let(UiImage::Url) ?: UiImage.Resource(R.drawable.ic_account_circle_cut)
private fun extractTitle( fun VkConvo.extractTitle(
conversation: VkConversation,
useContactName: Boolean, useContactName: Boolean,
resources: Resources resources: Resources
) = when (conversation.peerType) { ) = when (peerType) {
PeerType.USER -> { PeerType.USER -> {
if (isAccount(conversation.id)) { if (isAccount(id)) {
UiText.Resource(R.string.favorites) UiText.Resource(R.string.favorites)
} else { } else {
val userName = conversation.user?.let { user -> val userName = user?.let { user ->
if (useContactName) { if (useContactName) {
VkMemoryCache.getContact(user.id)?.name ?: user.fullName VkMemoryCache.getContact(user.id)?.name
} else { } else {
user.fullName user.fullName
} }
@@ -106,22 +62,22 @@ private fun extractTitle(
} }
} }
PeerType.GROUP -> UiText.Simple(conversation.group?.name.orDots()) PeerType.GROUP -> UiText.Simple(group?.name.orDots())
PeerType.CHAT -> UiText.Simple(conversation.title.orDots()) PeerType.CHAT -> UiText.Simple(title.orDots())
}.parseString(resources).orDots() }.parseString(resources).orDots()
private fun extractUnreadCount( fun extractUnreadCount(
lastMessage: VkMessage?, lastMessage: VkMessage?,
conversation: VkConversation convo: VkConvo
): String? = when { ): String? = when {
lastMessage?.isOut == false && conversation.isInRead() -> null lastMessage?.isOut == false && convo.isInRead() -> null
conversation.unreadCount == 0 -> null convo.unreadCount == 0 -> null
conversation.unreadCount < 1000 -> conversation.unreadCount.toString() convo.unreadCount < 1000 -> convo.unreadCount.toString()
else -> { else -> {
val exp = (ln(conversation.unreadCount.toDouble()) / ln(1000.0)).toInt() val exp = (ln(convo.unreadCount.toDouble()) / ln(1000.0)).toInt()
val suffix = "KMBT"[exp - 1] val suffix = "KMBT"[exp - 1]
val result = conversation.unreadCount / 1000.0.pow(exp.toDouble()) val result = convo.unreadCount / 1000.0.pow(exp.toDouble())
if (result.toLong().toDouble() == result) { if (result.toLong().toDouble() == result) {
String.format(Locale.getDefault(), "%.0f%s", result, suffix) String.format(Locale.getDefault(), "%.0f%s", result, suffix)
@@ -131,11 +87,12 @@ private fun extractUnreadCount(
} }
} }
private fun extractMessage( fun extractMessage(
resources: Resources, resources: Resources,
lastMessage: VkMessage?, lastMessage: VkMessage?,
peerId: Long, peerId: Long,
peerType: PeerType peerType: PeerType,
showPeer: Boolean = true
): AnnotatedString { ): AnnotatedString {
val youPrefix = UiText.Resource(R.string.you_message_prefix) val youPrefix = UiText.Resource(R.string.you_message_prefix)
.parseString(resources) .parseString(resources)
@@ -160,6 +117,8 @@ private fun extractMessage(
val messageText = lastMessage?.text.orEmpty() val messageText = lastMessage?.text.orEmpty()
val prefixText: AnnotatedString? = when { val prefixText: AnnotatedString? = when {
!showPeer -> null
actionMessage != null -> null actionMessage != null -> null
lastMessage == null -> null lastMessage == null -> null
@@ -226,16 +185,17 @@ private fun extractMessage(
.let { text -> .let { text ->
extractTextWithVisualizedMentions( extractTextWithVisualizedMentions(
isOut = lastMessage?.isOut == true, isOut = lastMessage?.isOut == true,
originalText = text originalText = text,
formatData = null
) )
} }
.let { text -> prefix + text } .let { text -> prefix + text.orEmpty() }
} }
return finalText return finalText
} }
private fun extractActionText( fun extractActionText(
lastMessage: VkMessage?, lastMessage: VkMessage?,
resources: Resources, resources: Resources,
youPrefix: String youPrefix: String
@@ -539,7 +499,7 @@ private fun extractAttachmentIcon(
} }
} }
private fun extractAttachmentText( fun extractAttachmentText(
resources: Resources, resources: Resources,
lastMessage: VkMessage? lastMessage: VkMessage?
): AnnotatedString? = when { ): AnnotatedString? = when {
@@ -649,7 +609,7 @@ private fun isAttachmentsHaveOneType(attachments: List<VkAttachment>): Boolean {
return true return true
} }
private fun extractForwardsText( fun extractForwardsText(
resources: Resources, resources: Resources,
lastMessage: VkMessage? lastMessage: VkMessage?
): AnnotatedString? = when { ): AnnotatedString? = when {
@@ -670,69 +630,7 @@ private fun extractForwardsText(
else -> null else -> null
} }
fun extractTextWithVisualizedMentions( fun getAttachmentUiText(
isOut: Boolean,
originalText: String
): AnnotatedString = buildAnnotatedString {
val regex = """\[(id|club)(\d+)\|([^]]+)]""".toRegex()
val mentions = mutableListOf<MentionIndex>()
var currentIndex = 0
val replacements = mutableListOf<Pair<IntRange, String>>()
val result = regex.replace(originalText) { matchResult ->
val idPrefix = matchResult.groups[1]?.value.orEmpty()
val startIndex = matchResult.range.first
val endIndex = matchResult.range.last
val id = matchResult.groups[2]?.value ?: ""
val replaced = matchResult.groups[3]?.value.orEmpty()
val indexRange =
(startIndex + currentIndex)..startIndex + currentIndex + replaced.length
replacements.add(indexRange to replaced)
mentions += MentionIndex(
id = id.toLongOrNull() ?: -1,
idPrefix = idPrefix,
indexRange = indexRange
)
currentIndex += replaced.length - (endIndex - startIndex + 1)
replaced
}
append(result)
mentions.forEach { mention ->
val startIndex = mention.indexRange.first
val endIndex = mention.indexRange.last
addStyle(
style = SpanStyle(color = Color.Red),
start = startIndex,
end = endIndex
)
addStringAnnotation(
tag = mention.idPrefix,
annotation = mention.id.toString(),
start = startIndex,
end = endIndex
)
}
}
data class MentionIndex(
val id: Long,
val idPrefix: String,
val indexRange: IntRange
)
private fun getAttachmentUiText(
attachment: VkAttachment, attachment: VkAttachment,
size: Int = 1, size: Int = 1,
): UiText { ): UiText {
@@ -787,7 +685,7 @@ private fun getAttachmentUiText(
}.let(UiText::Resource) }.let(UiText::Resource)
} }
private fun getAttachmentConversationIcon(message: VkMessage?): UiImage? { fun getAttachmentConvoIcon(message: VkMessage?): UiImage? {
return message?.attachments?.let { attachments -> return message?.attachments?.let { attachments ->
if (attachments.isEmpty()) return null if (attachments.isEmpty()) return null
if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) { if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) {
@@ -801,8 +699,8 @@ private fun getAttachmentConversationIcon(message: VkMessage?): UiImage? {
} }
} }
private fun extractBirthday(conversation: VkConversation): Boolean { fun extractBirthday(convo: VkConvo): Boolean {
val birthday = conversation.user?.birthday ?: return false val birthday = convo.user?.birthday ?: return false
val splitBirthday = birthday.split(".").mapNotNull(String::toIntOrNull) val splitBirthday = birthday.split(".").mapNotNull(String::toIntOrNull)
if (splitBirthday.isEmpty()) return false if (splitBirthday.isEmpty()) return false
@@ -822,25 +720,23 @@ private fun extractBirthday(conversation: VkConversation): Boolean {
} else false } else false
} }
private fun extractReadCondition( fun extractReadCondition(
conversation: VkConversation, convo: VkConvo,
lastMessage: VkMessage? lastMessage: VkMessage?
): Boolean = !conversation.isRead(lastMessage) ): Boolean = !convo.isRead(lastMessage)
private fun isAccount(peerId: Long) = peerId == UserConfig.userId fun extractInteractionText(
private fun extractInteractionText(
resources: Resources, resources: Resources,
conversation: VkConversation convo: VkConvo
): String? { ): String? {
val interactionType = InteractionType.parse(conversation.interactionType) val interactionType = InteractionType.parse(convo.interactionType)
val interactiveUsers = extractInteractionUsers(conversation) val interactiveUsers = extractInteractionUsers(convo)
val typingText = val typingText =
if (interactionType == null) { if (interactionType == null) {
null null
} else { } else {
if (!conversation.peerType.isChat() && interactiveUsers.size == 1) { if (!convo.peerType.isChat() && interactiveUsers.size == 1) {
when (interactionType) { when (interactionType) {
InteractionType.File -> R.string.chat_interaction_uploading_file InteractionType.File -> R.string.chat_interaction_uploading_file
InteractionType.Photo -> R.string.chat_interaction_uploading_photo InteractionType.Photo -> R.string.chat_interaction_uploading_photo
@@ -865,8 +761,8 @@ private fun extractInteractionText(
return typingText return typingText
} }
private fun extractInteractionUsers(conversation: VkConversation): List<String> { fun extractInteractionUsers(convo: VkConvo): List<String> {
return conversation.interactionIds.mapNotNull { id -> return convo.interactionIds.mapNotNull { id ->
when { when {
id > 0 -> VkMemoryCache.getUser(id)?.fullName id > 0 -> VkMemoryCache.getUser(id)?.fullName
id < 0 -> VkMemoryCache.getGroup(id)?.name id < 0 -> VkMemoryCache.getGroup(id)?.name
@@ -0,0 +1,47 @@
package dev.meloda.fast.domain.util
import android.content.res.Resources
import dev.meloda.fast.common.util.TimeUtils
import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.model.vk.ActionState
import dev.meloda.fast.ui.model.vk.ConvoOption
import dev.meloda.fast.ui.model.vk.UiConvo
import dev.meloda.fast.ui.util.ImmutableList
import dev.meloda.fast.ui.util.emptyImmutableList
fun VkConvo.asPresentation(
resources: Resources,
useContactName: Boolean,
isExpanded: Boolean = false,
options: ImmutableList<ConvoOption> = emptyImmutableList()
): UiConvo = UiConvo(
id = id,
lastMessageId = lastMessageId,
avatar = extractAvatar(),
title = extractTitle(useContactName, resources),
unreadCount = extractUnreadCount(lastMessage, this),
date = TimeUtils.getLocalizedTime(
date = (lastMessage?.date ?: -1) * 1000L,
yearShort = { resources.getString(R.string.year_short) },
monthShort = { resources.getString(R.string.month_short) },
weekShort = { resources.getString(R.string.week_short) },
dayShort = { resources.getString(R.string.day_short) },
now = { resources.getString(R.string.time_now) },
),
message = extractMessage(resources, lastMessage, id, peerType),
attachmentImage = if (lastMessage?.text == null) null
else getAttachmentConvoIcon(lastMessage),
isPinned = majorId > 0,
actionImageId = ActionState.parse(isPhantom, isCallInProgress).getResourceId(),
isBirthday = extractBirthday(this),
isUnread = !isRead(),
isAccount = isAccount(id),
isOnline = !isAccount(id) && user?.onlineStatus?.isOnline() == true,
lastMessage = lastMessage,
peerType = peerType,
interactionText = extractInteractionText(resources, this),
isExpanded = isExpanded,
isArchived = isArchived,
options = options
)
@@ -3,7 +3,7 @@ package dev.meloda.fast.domain.util
import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.ui.model.api.UiFriend import dev.meloda.fast.ui.model.vk.UiFriend
fun VkUser.asPresentation( fun VkUser.asPresentation(
useContactNames: Boolean = false useContactNames: Boolean = false
@@ -1,35 +1,21 @@
package dev.meloda.fast.messageshistory.util package dev.meloda.fast.domain.util
import android.content.res.Resources import android.content.res.Resources
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.AnnotatedString.Annotation
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.StringAnnotation
import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.buildAnnotatedString
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.text.style.TextDecoration
import dev.meloda.fast.common.extensions.orDots import dev.meloda.fast.common.extensions.orDots
import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.common.model.UiText import dev.meloda.fast.common.model.UiText
import dev.meloda.fast.common.model.parseString import dev.meloda.fast.common.model.parseString
import dev.meloda.fast.common.provider.ResourceProvider
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.model.api.PeerType.Companion.getPeerType
import dev.meloda.fast.messageshistory.model.SendingStatus
import dev.meloda.fast.messageshistory.model.UiItem
import dev.meloda.fast.model.api.PeerType
import dev.meloda.fast.model.api.domain.FormatDataType
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Locale import java.util.Locale
private fun isAccount(fromId: Long) = fromId == UserConfig.userId
fun VkMessage.extractAvatar() = when { fun VkMessage.extractAvatar() = when {
isUser() -> { isUser() -> {
if (isAccount(id)) null if (isAccount(id)) null
@@ -59,111 +45,15 @@ fun VkMessage.extractTitle(): String = when {
fun VkMessage.extractReplyTitle(): String? = replyMessage?.extractTitle() fun VkMessage.extractReplyTitle(): String? = replyMessage?.extractTitle()
// TODO: 24-Jun-25, Danil Nikolaev: improve fun VkMessage.extractReplySummary(resources: Resources): AnnotatedString? =
fun VkMessage.extractReplySummary(): String? = when (val message = replyMessage) { extractMessage(
null -> null resources = resources,
else -> { lastMessage = this,
when { peerId = peerId,
message.text != null -> message.text peerType = getPeerType(),
else -> null showPeer = false
}
}
}
fun VkConversation.extractAvatar(): UiImage = when (peerType) {
PeerType.USER -> {
if (isAccount(id)) null
else user?.photo200
}
PeerType.GROUP -> {
group?.photo200
}
PeerType.CHAT -> {
photo200
}
}?.let(UiImage::Url) ?: UiImage.Resource(R.drawable.ic_account_circle_cut)
fun VkConversation.extractTitle(
useContactName: Boolean,
resources: Resources
) = when (peerType) {
PeerType.USER -> {
if (isAccount(id)) {
UiText.Resource(R.string.favorites)
} else {
val userName = user?.let { user ->
if (useContactName) {
VkMemoryCache.getContact(user.id)?.name
} else {
user.fullName
}
}
UiText.Simple(userName.orDots())
}
}
PeerType.GROUP -> UiText.Simple(group?.name.orDots())
PeerType.CHAT -> UiText.Simple(title.orDots())
}.parseString(resources).orDots()
fun VkMessage.asPresentation(
conversation: VkConversation,
resourceProvider: ResourceProvider,
showName: Boolean,
prevMessage: VkMessage?,
nextMessage: VkMessage?,
showTimeInActionMessages: Boolean,
isSelected: Boolean
): UiItem = when {
action != null -> UiItem.ActionMessage(
id = id,
cmId = cmId,
text = extractActionText(
resources = resourceProvider.resources,
youPrefix = resourceProvider.getString(R.string.you_message_prefix),
showTime = showTimeInActionMessages
) ?: buildAnnotatedString { },
actionCmId = actionConversationMessageId
) )
else -> UiItem.Message(
id = id,
cmId = cmId,
text = extractTextWithVisualizedMentions(
isOut = isOut,
originalText = text,
formatData = formatData
),
isOut = isOut,
fromId = fromId,
date = extractDate(),
randomId = randomId,
isInChat = isPeerChat(),
name = extractTitle(),
showDate = true,
showAvatar = extractShowAvatar(nextMessage),
showName = showName && extractShowName(prevMessage),
avatar = extractAvatar(),
isEdited = updateTime != null,
isRead = isRead(conversation),
sendingStatus = when {
isFailed() -> SendingStatus.FAILED
id <= 0 -> SendingStatus.SENDING
else -> SendingStatus.SENT
},
isSelected = isSelected,
isPinned = isPinned,
isImportant = isImportant,
attachments = attachments?.ifEmpty { null }?.toImmutableList(),
replyCmId = replyMessage?.cmId,
replyTitle = extractReplyTitle(),
replySummary = extractReplySummary()
)
}
fun VkMessage.extractShowAvatar(nextMessage: VkMessage?): Boolean { fun VkMessage.extractShowAvatar(nextMessage: VkMessage?): Boolean {
if (isOut) return false if (isOut) return false
return nextMessage == null || nextMessage.fromId != fromId return nextMessage == null || nextMessage.fromId != fromId
@@ -569,153 +459,3 @@ fun VkMessage.extractActionText(
} }
} }
} }
// TODO: 04-Apr-25, Danil Nikolaev: get rid of method duplication
fun extractTextWithVisualizedMentions(
isOut: Boolean,
originalText: String?,
formatData: VkMessage.FormatData?
): AnnotatedString? {
if (originalText == null) return null
val annotations =
mutableListOf<AnnotatedString.Range<out Annotation>>()
val regex = """\[(id|club)(\d+)\|([^]]+)]""".toRegex()
val mentions = mutableListOf<MentionIndex>()
var currentIndex = 0
val replacements = mutableListOf<Pair<IntRange, String>>()
val newText = regex.replace(originalText) { matchResult ->
val idPrefix = matchResult.groups[1]?.value.orEmpty()
val startIndex = matchResult.range.first
val endIndex = matchResult.range.last
val id = matchResult.groups[2]?.value ?: ""
val replaced = matchResult.groups[3]?.value.orEmpty()
val indexRange =
(startIndex + currentIndex)..startIndex + currentIndex + replaced.length
replacements.add(indexRange to replaced)
mentions += MentionIndex(
id = id.toLongOrNull() ?: -1,
idPrefix = idPrefix,
indexRange = indexRange
)
currentIndex += replaced.length - (endIndex - startIndex + 1)
replaced
}
mentions.forEach { mention ->
val startIndex = mention.indexRange.first
val endIndex = mention.indexRange.last
annotations += if (isOut) {
AnnotatedString.Range(
item = SpanStyle(textDecoration = TextDecoration.Underline),
start = startIndex,
end = endIndex
)
} else {
AnnotatedString.Range(
item = SpanStyle(color = Color.Red),
start = startIndex,
end = endIndex
)
}
annotations += AnnotatedString.Range(
item = StringAnnotation(mention.id.toString()),
tag = mention.idPrefix,
start = startIndex,
end = endIndex
)
}
if (formatData == null) return AnnotatedString(text = newText, annotations = annotations)
var current = 0
val newOffsets = formatData.items.map { (offset, length) ->
val r = replacements.filter { (range, _) ->
(range - current) collidesWith (offset..<offset + length) || offset > range.first
}
current = r.sumOf { (range, _) -> range.last - range.first - 1 }
offset + current
}
formatData.items.forEachIndexed { index, item ->
val offset = newOffsets[index]
val spanStyle = when (item.type) {
FormatDataType.BOLD -> {
SpanStyle(fontWeight = FontWeight.SemiBold)
}
FormatDataType.ITALIC -> {
SpanStyle(fontStyle = FontStyle.Italic)
}
FormatDataType.UNDERLINE -> {
SpanStyle(textDecoration = TextDecoration.Underline)
}
FormatDataType.URL -> {
annotations += AnnotatedString.Range(
item = StringAnnotation(item.url.orEmpty()),
start = offset,
end = offset + item.length,
tag = newText.substring(offset, offset + item.length)
)
if (isOut) {
SpanStyle(
fontWeight = FontWeight.SemiBold,
textDecoration = TextDecoration.Underline
)
} else {
SpanStyle(
fontWeight = FontWeight.SemiBold,
color = Color.Red
)
}
}
}
annotations += AnnotatedString.Range(
item = spanStyle,
start = offset,
end = offset + item.length
)
}
return AnnotatedString(text = newText, annotations = annotations)
}
data class MentionIndex(
val id: Long,
val idPrefix: String,
val indexRange: IntRange
)
infix fun ClosedRange<Int>.collidesWith(other: ClosedRange<Int>): Boolean {
return this.start < other.endInclusive && other.start < this.endInclusive
}
operator fun ClosedRange<Int>.minus(other: ClosedRange<Int>): ClosedRange<Int> {
return (this.start - other.start)..(this.endInclusive - other.endInclusive)
}
operator fun ClosedRange<Int>.minus(other: Int): ClosedRange<Int> {
return (this.start - other)..(this.endInclusive - other)
}
@@ -0,0 +1,65 @@
package dev.meloda.fast.domain.util
import androidx.compose.ui.text.buildAnnotatedString
import dev.meloda.fast.common.provider.ResourceProvider
import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.model.vk.MessageUiItem
import dev.meloda.fast.ui.model.vk.SendingStatus
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
fun VkMessage.asPresentation(
convo: VkConvo,
resourceProvider: ResourceProvider,
showName: Boolean,
prevMessage: VkMessage?,
nextMessage: VkMessage?,
showTimeInActionMessages: Boolean,
isSelected: Boolean
): MessageUiItem = when {
action != null -> MessageUiItem.ActionMessage(
id = id,
cmId = cmId,
text = extractActionText(
resources = resourceProvider.resources,
youPrefix = resourceProvider.getString(R.string.you_message_prefix),
showTime = showTimeInActionMessages
) ?: buildAnnotatedString { },
actionCmId = actionCmId
)
else -> MessageUiItem.Message(
id = id,
cmId = cmId,
text = extractTextWithVisualizedMentions(
isOut = isOut,
originalText = text,
formatData = formatData
),
isOut = isOut,
fromId = fromId,
date = extractDate(),
randomId = randomId,
isInChat = isPeerChat(),
name = extractTitle(),
showDate = true,
showAvatar = extractShowAvatar(nextMessage),
showName = showName && extractShowName(prevMessage),
avatar = extractAvatar(),
isEdited = updateTime != null,
isRead = isRead(convo),
sendingStatus = when {
isFailed() -> SendingStatus.FAILED
id <= 0 -> SendingStatus.SENDING
else -> SendingStatus.SENT
},
isSelected = isSelected,
isPinned = isPinned,
isImportant = isImportant,
attachments = attachments?.ifEmpty { null }?.toImmutableList(),
replyCmId = replyMessage?.cmId,
replyTitle = extractReplyTitle(),
replySummary = replyMessage?.extractReplySummary(resourceProvider.resources)
)
}
@@ -0,0 +1,177 @@
package dev.meloda.fast.domain.util
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.StringAnnotation
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import com.conena.nanokt.collections.indexOfFirstOrNull
import dev.meloda.fast.common.extensions.collidesWith
import dev.meloda.fast.common.extensions.minus
import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.model.api.domain.FormatDataType
import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.ui.model.vk.MentionIndex
import dev.meloda.fast.ui.model.vk.MessageUiItem
fun emptyAnnotatedString(): AnnotatedString = AnnotatedString(text = "")
fun AnnotatedString?.orEmpty(): AnnotatedString = this ?: emptyAnnotatedString()
fun String.annotated(): AnnotatedString = AnnotatedString(text = this)
fun isAccount(id: Long) = id == UserConfig.userId
fun extractTextWithVisualizedMentions(
isOut: Boolean,
originalText: String?,
formatData: VkMessage.FormatData?
): AnnotatedString? {
if (originalText == null) return null
val annotations =
mutableListOf<AnnotatedString.Range<out androidx.compose.ui.text.AnnotatedString.Annotation>>()
val regex = """\[(id|club)(\d+)\|([^]]+)]""".toRegex()
val mentions = mutableListOf<MentionIndex>()
var currentIndex = 0
val replacements = mutableListOf<Pair<IntRange, String>>()
val newText = regex.replace(originalText) { matchResult ->
val idPrefix = matchResult.groups[1]?.value.orEmpty()
val startIndex = matchResult.range.first
val endIndex = matchResult.range.last
val id = matchResult.groups[2]?.value ?: ""
val replaced = matchResult.groups[3]?.value.orEmpty()
val indexRange =
(startIndex + currentIndex)..startIndex + currentIndex + replaced.length
replacements.add(indexRange to replaced)
mentions += MentionIndex(
id = id.toLongOrNull() ?: -1,
idPrefix = idPrefix,
indexRange = indexRange
)
currentIndex += replaced.length - (endIndex - startIndex + 1)
replaced
}
mentions.forEach { mention ->
val startIndex = mention.indexRange.first
val endIndex = mention.indexRange.last
annotations += if (isOut) {
AnnotatedString.Range(
item = SpanStyle(textDecoration = TextDecoration.Underline),
start = startIndex,
end = endIndex
)
} else {
AnnotatedString.Range(
item = SpanStyle(color = Color.Red),
start = startIndex,
end = endIndex
)
}
annotations += AnnotatedString.Range(
item = StringAnnotation(mention.id.toString()),
tag = mention.idPrefix,
start = startIndex,
end = endIndex
)
}
if (formatData == null) {
return AnnotatedString(text = newText, annotations = annotations)
}
var current = 0
val newOffsets = formatData.items.map { (offset, length) ->
val r = replacements.filter { (range, _) ->
(range - current) collidesWith (offset..<offset + length) || offset > range.first
}
current = r.sumOf { (range, _) -> range.last - range.first - 1 }
offset + current
}
formatData.items.forEachIndexed { index, item ->
val offset = newOffsets[index]
val spanStyle = when (item.type) {
FormatDataType.BOLD -> {
SpanStyle(fontWeight = FontWeight.SemiBold)
}
FormatDataType.ITALIC -> {
SpanStyle(fontStyle = FontStyle.Italic)
}
FormatDataType.UNDERLINE -> {
SpanStyle(textDecoration = TextDecoration.Underline)
}
FormatDataType.URL -> {
annotations += AnnotatedString.Range(
item = StringAnnotation(item.url.orEmpty()),
start = offset,
end = offset + item.length,
tag = newText.substring(offset, offset + item.length)
)
if (isOut) {
SpanStyle(
fontWeight = FontWeight.SemiBold,
textDecoration = TextDecoration.Underline
)
} else {
SpanStyle(
fontWeight = FontWeight.SemiBold,
color = Color.Red
)
}
}
}
annotations += AnnotatedString.Range(
item = spanStyle,
start = offset,
end = offset + item.length
)
}
return AnnotatedString(text = newText, annotations = annotations)
}
fun List<MessageUiItem>.firstMessage(): MessageUiItem.Message =
filterIsInstance<MessageUiItem.Message>().first()
fun List<MessageUiItem>.firstMessageOrNull(): MessageUiItem.Message? =
filterIsInstance<MessageUiItem.Message>().firstOrNull()
fun List<MessageUiItem>.indexOfMessageById(messageId: Long): Int =
indexOfFirst { it.id == messageId }
fun List<MessageUiItem>.findMessageById(messageId: Long): MessageUiItem.Message? =
firstOrNull { it.id == messageId } as MessageUiItem.Message?
fun List<MessageUiItem>.indexOfMessageByCmId(cmId: Long): Int? =
indexOfFirstOrNull { it.cmId == cmId }
fun List<MessageUiItem>.findMessageByCmId(cmId: Long): MessageUiItem.Message =
first { it.cmId == cmId } as MessageUiItem.Message
@@ -1,6 +1,6 @@
package dev.meloda.fast.model package dev.meloda.fast.model
enum class ConversationFlags(val value: Int) { enum class ConvoFlags(val value: Int) {
DISABLE_PUSH(16), DISABLE_PUSH(16),
DISABLE_SOUND(32), DISABLE_SOUND(32),
INCOMING_CHAT_REQUEST(256), INCOMING_CHAT_REQUEST(256),
@@ -17,10 +17,10 @@ enum class ConversationFlags(val value: Int) {
companion object { companion object {
fun parse(mask: Int): List<ConversationFlags> { fun parse(mask: Int): List<ConvoFlags> {
val flags = mutableListOf<ConversationFlags>() val flags = mutableListOf<ConvoFlags>()
ConversationFlags.entries.forEach { flag -> ConvoFlags.entries.forEach { flag ->
if (mask and flag.value > 0) { if (mask and flag.value > 0) {
flags.add(flag) flags.add(flag)
} }
@@ -1,5 +1,5 @@
package dev.meloda.fast.model package dev.meloda.fast.model
enum class ConversationsFilter { enum class ConvosFilter {
ALL, UNREAD, ARCHIVE, BUSINESS_NOTIFY ALL, UNREAD, ARCHIVE, BUSINESS_NOTIFY
} }
@@ -1,6 +1,6 @@
package dev.meloda.fast.model package dev.meloda.fast.model
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
sealed interface LongPollParsedEvent { sealed interface LongPollParsedEvent {
@@ -92,7 +92,7 @@ sealed interface LongPollParsedEvent {
) : LongPollParsedEvent ) : LongPollParsedEvent
data class ChatArchived( data class ChatArchived(
val conversation: VkConversation, val convo: VkConvo,
val archived: Boolean val archived: Boolean
) : LongPollParsedEvent ) : LongPollParsedEvent
} }
@@ -1,5 +1,7 @@
package dev.meloda.fast.model.api package dev.meloda.fast.model.api
import dev.meloda.fast.model.api.domain.VkMessage
enum class PeerType(val value: String) { enum class PeerType(val value: String) {
USER("user"), USER("user"),
GROUP("group"), GROUP("group"),
@@ -13,5 +15,14 @@ enum class PeerType(val value: String) {
fun parse(type: String): PeerType { fun parse(type: String): PeerType {
return entries.first { it.value == type } return entries.first { it.value == type }
} }
fun VkMessage.getPeerType(): PeerType {
return when {
peerId > 2_000_000_000 -> CHAT
peerId > 0 -> USER
peerId < 0 -> GROUP
else -> throw IllegalArgumentException("Unknown peer type for peerId: 0")
}
}
} }
} }
@@ -8,7 +8,7 @@ import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
data class VkAttachmentHistoryMessageData( data class VkAttachmentHistoryMessageData(
@Json(name = "message_id") val messageId: Long, @Json(name = "message_id") val messageId: Long,
@Json(name = "date") val date: Int, @Json(name = "date") val date: Int,
@Json(name = "cmid") val conversationMessageId: Long, @Json(name = "cmid") val cmId: Long,
@Json(name = "from_id") val fromId: Long, @Json(name = "from_id") val fromId: Long,
@Json(name = "position") val position: Int, @Json(name = "position") val position: Int,
@Json(name = "attachment") val attachment: VkAttachmentItemData @Json(name = "attachment") val attachment: VkAttachmentItemData
@@ -16,7 +16,7 @@ data class VkAttachmentHistoryMessageData(
fun toDomain(): VkAttachmentHistoryMessage = VkAttachmentHistoryMessage( fun toDomain(): VkAttachmentHistoryMessage = VkAttachmentHistoryMessage(
messageId = messageId, messageId = messageId,
conversationMessageId = conversationMessageId, cmId = cmId,
date = date, date = date,
fromId = fromId, fromId = fromId,
position = position, position = position,
@@ -3,19 +3,19 @@ package dev.meloda.fast.model.api.data
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.PeerType import dev.meloda.fast.model.api.PeerType
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkMessage import dev.meloda.fast.model.api.domain.VkMessage
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class VkConversationData( data class VkConvoData(
@Json(name = "peer") val peer: Peer, @Json(name = "peer") val peer: Peer,
@Json(name = "last_message_id") val lastMessageId: Long?, @Json(name = "last_message_id") val lastMessageId: Long?,
@Json(name = "in_read") val inRead: Long, @Json(name = "in_read") val inRead: Long,
@Json(name = "out_read") val outRead: Long, @Json(name = "out_read") val outRead: Long,
@Json(name = "in_read_cmid") val inReadConversationMessageId: Long, @Json(name = "in_read_cmid") val inReadCmId: Long,
@Json(name = "out_read_cmid") val outReadConversationMessageId: Long, @Json(name = "out_read_cmid") val outReadCmId: Long,
@Json(name = "sort_id") val sortId: SortId, @Json(name = "sort_id") val sortId: SortId,
@Json(name = "last_conversation_message_id") val lastConversationMessageId: Long, @Json(name = "last_conversation_message_id") val lastCmId: Long,
@Json(name = "is_marked_unread") val isMarkedUnread: Boolean, @Json(name = "is_marked_unread") val isMarkedUnread: Boolean,
@Json(name = "important") val important: Boolean, @Json(name = "important") val important: Boolean,
@Json(name = "push_settings") val pushSettings: PushSettings?, @Json(name = "push_settings") val pushSettings: PushSettings?,
@@ -111,7 +111,7 @@ data class VkConversationData(
fun asDomain( fun asDomain(
lastMessage: VkMessage? = null, lastMessage: VkMessage? = null,
): VkConversation = VkConversation( ): VkConvo = VkConvo(
id = peer.id, id = peer.id,
localId = peer.localId, localId = peer.localId,
title = chatSettings?.title, title = chatSettings?.title,
@@ -120,7 +120,7 @@ data class VkConversationData(
photo200 = chatSettings?.photo?.photo200, photo200 = chatSettings?.photo?.photo200,
isCallInProgress = callInProgress != null, isCallInProgress = callInProgress != null,
isPhantom = chatSettings?.isDisappearing == true, isPhantom = chatSettings?.isDisappearing == true,
lastCmId = lastConversationMessageId, lastCmId = lastCmId,
inRead = inRead, inRead = inRead,
outRead = outRead, outRead = outRead,
lastMessageId = lastMessageId, lastMessageId = lastMessageId,
@@ -132,8 +132,8 @@ data class VkConversationData(
canChangePin = chatSettings?.acl?.canChangePin == true, canChangePin = chatSettings?.acl?.canChangePin == true,
canChangeInfo = chatSettings?.acl?.canChangeInfo == true, canChangeInfo = chatSettings?.acl?.canChangeInfo == true,
pinnedMessageId = chatSettings?.pinnedMessage?.id, pinnedMessageId = chatSettings?.pinnedMessage?.id,
inReadCmId = inReadConversationMessageId, inReadCmId = inReadCmId,
outReadCmId = outReadConversationMessageId, outReadCmId = outReadCmId,
interactionType = -1, interactionType = -1,
interactionIds = emptyList(), interactionIds = emptyList(),
peerType = PeerType.parse(peer.type), peerType = PeerType.parse(peer.type),
@@ -56,7 +56,7 @@ data class VkMessageData(
@Json(name = "type") val type: String, @Json(name = "type") val type: String,
@Json(name = "member_id") val memberId: Long?, @Json(name = "member_id") val memberId: Long?,
@Json(name = "text") val text: String?, @Json(name = "text") val text: String?,
@Json(name = "conversation_message_id") val conversationMessageId: Long?, @Json(name = "conversation_message_id") val cmId: Long?,
@Json(name = "message") val message: String? @Json(name = "message") val message: String?
) )
@@ -102,7 +102,7 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage(
action = VkMessage.Action.parse(action?.type), action = VkMessage.Action.parse(action?.type),
actionMemberId = action?.memberId, actionMemberId = action?.memberId,
actionText = action?.text, actionText = action?.text,
actionConversationMessageId = action?.conversationMessageId, actionCmId = action?.cmId,
actionMessage = action?.message, actionMessage = action?.message,
geoType = geo?.type, geoType = geo?.type,
isImportant = important == true, isImportant = important == true,
@@ -12,7 +12,7 @@ data class VkPinnedMessageData(
@Json(name = "from_id") val fromId: Long, @Json(name = "from_id") val fromId: Long,
@Json(name = "out") val out: Boolean?, @Json(name = "out") val out: Boolean?,
@Json(name = "text") val text: String, @Json(name = "text") val text: String,
@Json(name = "conversation_message_id") val conversationMessageId: Long, @Json(name = "conversation_message_id") val cmId: Long,
@Json(name = "fwd_messages") val forwards: List<VkMessageData>?, @Json(name = "fwd_messages") val forwards: List<VkMessageData>?,
@Json(name = "important") val important: Boolean = false, @Json(name = "important") val important: Boolean = false,
@Json(name = "random_id") val randomId: Long = 0, @Json(name = "random_id") val randomId: Long = 0,
@@ -28,7 +28,7 @@ data class VkPinnedMessageData(
fun mapToDomain(): VkMessage = VkMessage( fun mapToDomain(): VkMessage = VkMessage(
id = id ?: -1, id = id ?: -1,
cmId = conversationMessageId, cmId = cmId,
text = text.ifBlank { null }, text = text.ifBlank { null },
isOut = out == true, isOut = out == true,
peerId = peerId ?: -1, peerId = peerId ?: -1,
@@ -38,7 +38,7 @@ data class VkPinnedMessageData(
action = VkMessage.Action.parse(action?.type), action = VkMessage.Action.parse(action?.type),
actionMemberId = action?.memberId, actionMemberId = action?.memberId,
actionText = action?.text, actionText = action?.text,
actionConversationMessageId = action?.conversationMessageId, actionCmId = action?.cmId,
actionMessage = action?.message, actionMessage = action?.message,
geoType = geo?.type, geoType = geo?.type,
isImportant = important, isImportant = important,
@@ -2,7 +2,7 @@ package dev.meloda.fast.model.api.domain
data class VkAttachmentHistoryMessage( data class VkAttachmentHistoryMessage(
val messageId: Long, val messageId: Long,
val conversationMessageId: Long, val cmId: Long,
val date: Int, val date: Int,
val fromId: Long, val fromId: Long,
val position: Int, val position: Int,
@@ -1,9 +1,9 @@
package dev.meloda.fast.model.api.domain package dev.meloda.fast.model.api.domain
import dev.meloda.fast.model.api.PeerType import dev.meloda.fast.model.api.PeerType
import dev.meloda.fast.model.database.VkConversationEntity import dev.meloda.fast.model.database.VkConvoEntity
data class VkConversation( data class VkConvo(
val id: Long, val id: Long,
val localId: Long, val localId: Long,
val ownerId: Long?, val ownerId: Long?,
@@ -54,7 +54,7 @@ data class VkConversation(
} }
companion object { companion object {
val EMPTY: VkConversation = VkConversation( val EMPTY: VkConvo = VkConvo(
id = -1, id = -1,
localId = -1, localId = -1,
ownerId = null, ownerId = null,
@@ -90,7 +90,7 @@ data class VkConversation(
} }
} }
fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity( fun VkConvo.asEntity(): VkConvoEntity = VkConvoEntity(
id = id, id = id,
localId = localId, localId = localId,
ownerId = ownerId, ownerId = ownerId,
@@ -99,7 +99,7 @@ fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity(
photo100 = photo100, photo100 = photo100,
photo200 = photo200, photo200 = photo200,
isPhantom = isPhantom, isPhantom = isPhantom,
lastConversationMessageId = lastCmId, lastCmId = lastCmId,
inReadCmId = inReadCmId, inReadCmId = inReadCmId,
outReadCmId = outReadCmId, outReadCmId = outReadCmId,
inRead = inRead, inRead = inRead,
@@ -16,7 +16,7 @@ data class VkMessage(
val action: Action?, val action: Action?,
val actionMemberId: Long?, val actionMemberId: Long?,
val actionText: String?, val actionText: String?,
val actionConversationMessageId: Long?, val actionCmId: Long?,
val actionMessage: String?, val actionMessage: String?,
val updateTime: Int?, val updateTime: Int?,
@@ -44,9 +44,9 @@ data class VkMessage(
fun isGroup() = fromId < 0 fun isGroup() = fromId < 0
fun isRead(conversation: VkConversation): Boolean = when { fun isRead(convo: VkConvo): Boolean = when {
id <= 0 -> false id <= 0 -> false
else -> conversation.isRead(this) else -> convo.isRead(this)
} }
fun hasAttachments(): Boolean = attachments.orEmpty().isNotEmpty() fun hasAttachments(): Boolean = attachments.orEmpty().isNotEmpty()
@@ -98,7 +98,7 @@ data class VkMessage(
fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity( fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity(
id = id, id = id,
conversationMessageId = cmId, cmId = cmId,
text = text, text = text,
isOut = isOut, isOut = isOut,
peerId = peerId, peerId = peerId,
@@ -108,7 +108,7 @@ fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity(
action = action?.value, action = action?.value,
actionMemberId = actionMemberId, actionMemberId = actionMemberId,
actionText = actionText, actionText = actionText,
actionConversationMessageId = actionConversationMessageId, actionCmId = actionCmId,
actionMessage = actionMessage, actionMessage = actionMessage,
updateTime = updateTime, updateTime = updateTime,
important = isImportant, important = isImportant,
@@ -1,12 +1,12 @@
package dev.meloda.fast.model.api.requests package dev.meloda.fast.model.api.requests
import dev.meloda.fast.model.ConversationsFilter import dev.meloda.fast.model.ConvosFilter
data class ConversationsGetRequest( data class ConvosGetRequest(
val count: Int? = null, val count: Int? = null,
val offset: Int? = null, val offset: Int? = null,
val fields: String = "", val fields: String = "",
val filter: ConversationsFilter = ConversationsFilter.ALL, val filter: ConvosFilter = ConvosFilter.ALL,
val extended: Boolean? = true, val extended: Boolean? = true,
val startMessageId: Long? = null val startMessageId: Long? = null
) { ) {
@@ -115,7 +115,7 @@ data class MessagesGetLongPollServerRequest(
data class MessagesPinMessageRequest( data class MessagesPinMessageRequest(
val peerId: Long, val peerId: Long,
val messageId: Long? = null, val messageId: Long? = null,
val conversationMessageId: Long? = null val cmId: Long? = null
) { ) {
val map: Map<String, String> val map: Map<String, String>
@@ -123,7 +123,7 @@ data class MessagesPinMessageRequest(
"peer_id" to peerId.toString() "peer_id" to peerId.toString()
).apply { ).apply {
messageId?.let { this["message_id"] = it.toString() } messageId?.let { this["message_id"] = it.toString() }
conversationMessageId?.let { this["conversation_message_id"] = it.toString() } cmId?.let { this["conversation_message_id"] = it.toString() }
} }
} }
@@ -136,7 +136,7 @@ data class MessagesUnpinMessageRequest(val peerId: Long) {
data class MessagesDeleteRequest( data class MessagesDeleteRequest(
val peerId: Long, val peerId: Long,
val messagesIds: List<Long>? = null, val messagesIds: List<Long>? = null,
val conversationsMessagesIds: List<Long>? = null, val cmIds: List<Long>? = null,
val isSpam: Boolean? = null, val isSpam: Boolean? = null,
val deleteForAll: Boolean? = null val deleteForAll: Boolean? = null
) { ) {
@@ -149,7 +149,7 @@ data class MessagesDeleteRequest(
deleteForAll?.let { this["delete_for_all"] = it.asInt().toString() } deleteForAll?.let { this["delete_for_all"] = it.asInt().toString() }
messagesIds?.let { this["message_ids"] = it.joinToString() } messagesIds?.let { this["message_ids"] = it.joinToString() }
conversationsMessagesIds?.let { cmIds?.let {
this["conversation_message_ids"] = it.joinToString() this["conversation_message_ids"] = it.joinToString()
} }
} }
@@ -228,7 +228,7 @@ data class MessagesGetChatRequest(
} }
data class MessagesGetConversationMembersRequest( data class MessagesGetConvoMembersRequest(
val peerId: Long, val peerId: Long,
val offset: Int? = null, val offset: Int? = null,
val count: Int? = null, val count: Int? = null,
@@ -267,14 +267,14 @@ data class MessagesGetHistoryAttachmentsRequest(
val offset: Int?, val offset: Int?,
val preserveOrder: Boolean?, val preserveOrder: Boolean?,
val attachmentTypes: List<String>, val attachmentTypes: List<String>,
val conversationMessageId: Long, val cmId: Long,
val fields: String? val fields: String?
) { ) {
val map = mutableMapOf( val map = mutableMapOf(
"peer_id" to peerId.toString(), "peer_id" to peerId.toString(),
"attachment_types" to attachmentTypes.joinToString(","), "attachment_types" to attachmentTypes.joinToString(","),
"cmid" to conversationMessageId.toString() "cmid" to cmId.toString()
).apply { ).apply {
extended?.let { this["extended"] = it.toString() } extended?.let { this["extended"] = it.toString() }
count?.let { this["count"] = it.toString() } count?.let { this["count"] = it.toString() }
@@ -3,15 +3,15 @@ package dev.meloda.fast.model.api.responses
import com.squareup.moshi.Json import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.data.VkContactData import dev.meloda.fast.model.api.data.VkContactData
import dev.meloda.fast.model.api.data.VkConversationData import dev.meloda.fast.model.api.data.VkConvoData
import dev.meloda.fast.model.api.data.VkGroupData import dev.meloda.fast.model.api.data.VkGroupData
import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.data.VkMessageData
import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.data.VkUserData
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ConversationsGetResponse( data class ConvosGetResponse(
@Json(name = "count") val count: Int, @Json(name = "count") val count: Int,
@Json(name = "items") val items: List<ConversationsResponseItem>, @Json(name = "items") val items: List<ConvosResponseItem>,
@Json(name = "unread_count") val unreadCount: Int?, @Json(name = "unread_count") val unreadCount: Int?,
@Json(name = "profiles") val profiles: List<VkUserData>?, @Json(name = "profiles") val profiles: List<VkUserData>?,
@Json(name = "groups") val groups: List<VkGroupData>?, @Json(name = "groups") val groups: List<VkGroupData>?,
@@ -19,21 +19,21 @@ data class ConversationsGetResponse(
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ConversationsGetByIdResponse( data class ConvosGetByIdResponse(
@Json(name = "count") val count: Int, @Json(name = "count") val count: Int,
@Json(name = "items") val items: List<VkConversationData>, @Json(name = "items") val items: List<VkConvoData>,
@Json(name = "profiles") val profiles: List<VkUserData>?, @Json(name = "profiles") val profiles: List<VkUserData>?,
@Json(name = "groups") val groups: List<VkGroupData>?, @Json(name = "groups") val groups: List<VkGroupData>?,
@Json(name = "contacts") val contacts: List<VkContactData>? @Json(name = "contacts") val contacts: List<VkContactData>?
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ConversationsResponseItem( data class ConvosResponseItem(
@Json(name = "conversation") val conversation: VkConversationData, @Json(name = "conversation") val convo: VkConvoData,
@Json(name = "last_message") val lastMessage: VkMessageData? @Json(name = "last_message") val lastMessage: VkMessageData?
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class ConversationsDeleteResponse( data class ConvosDeleteResponse(
@Json(name = "last_deleted_id") val lastDeletedId: Long @Json(name = "last_deleted_id") val lastDeletedId: Long
) )
@@ -5,7 +5,7 @@ import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.data.VkAttachmentHistoryMessageData import dev.meloda.fast.model.api.data.VkAttachmentHistoryMessageData
import dev.meloda.fast.model.api.data.VkChatMemberData import dev.meloda.fast.model.api.data.VkChatMemberData
import dev.meloda.fast.model.api.data.VkContactData import dev.meloda.fast.model.api.data.VkContactData
import dev.meloda.fast.model.api.data.VkConversationData import dev.meloda.fast.model.api.data.VkConvoData
import dev.meloda.fast.model.api.data.VkGroupData import dev.meloda.fast.model.api.data.VkGroupData
import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.data.VkMessageData
import dev.meloda.fast.model.api.data.VkUserData import dev.meloda.fast.model.api.data.VkUserData
@@ -14,7 +14,7 @@ import dev.meloda.fast.model.api.data.VkUserData
data class MessagesGetHistoryResponse( data class MessagesGetHistoryResponse(
val count: Int, val count: Int,
val items: List<VkMessageData>, val items: List<VkMessageData>,
val conversations: List<VkConversationData>?, val convos: List<VkConvoData>?,
val profiles: List<VkUserData>?, val profiles: List<VkUserData>?,
val groups: List<VkGroupData>?, val groups: List<VkGroupData>?,
val contacts: List<VkContactData>? val contacts: List<VkContactData>?
@@ -30,7 +30,7 @@ data class MessagesGetByIdResponse(
) )
@JsonClass(generateAdapter = true) @JsonClass(generateAdapter = true)
data class MessagesGetConversationMembersResponse( data class MessagesGetConvoMembersResponse(
val count: Int, val count: Int,
val items: List<VkChatMemberData>?, val items: List<VkChatMemberData>?,
val profiles: List<VkUserData>?, val profiles: List<VkUserData>?,
@@ -3,8 +3,8 @@ package dev.meloda.fast.model.database
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Relation import androidx.room.Relation
data class ConversationWithMessage( data class ConvoWithMessage(
@Embedded val conversation: VkConversationEntity, @Embedded val convo: VkConvoEntity,
@Relation( @Relation(
parentColumn = "lastMessageId", parentColumn = "lastMessageId",
entityColumn = "id" entityColumn = "id"
@@ -3,10 +3,10 @@ package dev.meloda.fast.model.database
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import dev.meloda.fast.model.api.PeerType import dev.meloda.fast.model.api.PeerType
import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.model.api.domain.VkConvo
@Entity(tableName = "conversations") @Entity(tableName = "convos")
data class VkConversationEntity( data class VkConvoEntity(
@PrimaryKey val id: Long, @PrimaryKey val id: Long,
val localId: Long, val localId: Long,
val ownerId: Long?, val ownerId: Long?,
@@ -15,7 +15,7 @@ data class VkConversationEntity(
val photo100: String?, val photo100: String?,
val photo200: String?, val photo200: String?,
val isPhantom: Boolean, val isPhantom: Boolean,
val lastConversationMessageId: Long, val lastCmId: Long,
val inReadCmId: Long, val inReadCmId: Long,
val outReadCmId: Long, val outReadCmId: Long,
val inRead: Long, val inRead: Long,
@@ -32,7 +32,7 @@ data class VkConversationEntity(
val isArchived: Boolean val isArchived: Boolean
) )
fun VkConversationEntity.asExternalModel(): VkConversation = VkConversation( fun VkConvoEntity.asExternalModel(): VkConvo = VkConvo(
id = id, id = id,
localId = localId, localId = localId,
ownerId = ownerId, ownerId = ownerId,
@@ -42,7 +42,7 @@ fun VkConversationEntity.asExternalModel(): VkConversation = VkConversation(
photo200 = photo200, photo200 = photo200,
isCallInProgress = false, isCallInProgress = false,
isPhantom = isPhantom, isPhantom = isPhantom,
lastCmId = lastConversationMessageId, lastCmId = lastCmId,
inReadCmId = inReadCmId, inReadCmId = inReadCmId,
outReadCmId = outReadCmId, outReadCmId = outReadCmId,
inRead = inRead, inRead = inRead,
@@ -8,7 +8,7 @@ import dev.meloda.fast.model.api.domain.VkUnknownAttachment
@Entity(tableName = "messages") @Entity(tableName = "messages")
data class VkMessageEntity( data class VkMessageEntity(
@PrimaryKey val id: Long, @PrimaryKey val id: Long,
val conversationMessageId: Long, val cmId: Long,
val text: String?, val text: String?,
val isOut: Boolean, val isOut: Boolean,
val peerId: Long, val peerId: Long,
@@ -18,7 +18,7 @@ data class VkMessageEntity(
val action: String?, val action: String?,
val actionMemberId: Long?, val actionMemberId: Long?,
val actionText: String?, val actionText: String?,
val actionConversationMessageId: Long?, val actionCmId: Long?,
val actionMessage: String?, val actionMessage: String?,
val updateTime: Int?, val updateTime: Int?,
val important: Boolean, val important: Boolean,
@@ -32,7 +32,7 @@ data class VkMessageEntity(
fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage( fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage(
id = id, id = id,
cmId = conversationMessageId, cmId = cmId,
text = text, text = text,
isOut = isOut, isOut = isOut,
peerId = peerId, peerId = peerId,
@@ -42,7 +42,7 @@ fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage(
action = VkMessage.Action.parse(action), action = VkMessage.Action.parse(action),
actionMemberId = actionMemberId, actionMemberId = actionMemberId,
actionText = actionText, actionText = actionText,
actionConversationMessageId = actionConversationMessageId, actionCmId = actionCmId,
actionMessage = actionMessage, actionMessage = actionMessage,
updateTime = updateTime, updateTime = updateTime,
isImportant = important, isImportant = important,
@@ -16,7 +16,7 @@ import dev.meloda.fast.network.interceptor.VersionInterceptor
import dev.meloda.fast.network.service.account.AccountService import dev.meloda.fast.network.service.account.AccountService
import dev.meloda.fast.network.service.audios.AudiosService import dev.meloda.fast.network.service.audios.AudiosService
import dev.meloda.fast.network.service.auth.AuthService import dev.meloda.fast.network.service.auth.AuthService
import dev.meloda.fast.network.service.conversations.ConversationsService import dev.meloda.fast.network.service.convos.ConvosService
import dev.meloda.fast.network.service.files.FilesService import dev.meloda.fast.network.service.files.FilesService
import dev.meloda.fast.network.service.friends.FriendsService import dev.meloda.fast.network.service.friends.FriendsService
import dev.meloda.fast.network.service.longpoll.LongPollService import dev.meloda.fast.network.service.longpoll.LongPollService
@@ -80,7 +80,7 @@ val networkModule = module {
single { service(AccountService::class.java) } single { service(AccountService::class.java) }
single { service(AudiosService::class.java) } single { service(AudiosService::class.java) }
single { service(ConversationsService::class.java) } single { service(ConvosService::class.java) }
single { service(FilesService::class.java) } single { service(FilesService::class.java) }
single { service(LongPollService::class.java) } single { service(LongPollService::class.java) }
single { service(MessagesService::class.java) } single { service(MessagesService::class.java) }
@@ -1,61 +1,61 @@
package dev.meloda.fast.network.service.conversations package dev.meloda.fast.network.service.convos
import com.slack.eithernet.ApiResult import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.api.responses.ConversationsDeleteResponse import dev.meloda.fast.model.api.responses.ConvosDeleteResponse
import dev.meloda.fast.model.api.responses.ConversationsGetByIdResponse import dev.meloda.fast.model.api.responses.ConvosGetByIdResponse
import dev.meloda.fast.model.api.responses.ConversationsGetResponse import dev.meloda.fast.model.api.responses.ConvosGetResponse
import dev.meloda.fast.network.ApiResponse import dev.meloda.fast.network.ApiResponse
import dev.meloda.fast.network.RestApiError import dev.meloda.fast.network.RestApiError
import retrofit2.http.FieldMap import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST import retrofit2.http.POST
interface ConversationsService { interface ConvosService {
@FormUrlEncoded @FormUrlEncoded
@POST(ConversationsUrls.GET) @POST(ConvosUrls.GET)
suspend fun getConversations( suspend fun getConvos(
@FieldMap params: Map<String, String> @FieldMap params: Map<String, String>
): ApiResult<ApiResponse<ConversationsGetResponse>, RestApiError> ): ApiResult<ApiResponse<ConvosGetResponse>, RestApiError>
@FormUrlEncoded @FormUrlEncoded
@POST(ConversationsUrls.GET_BY_ID) @POST(ConvosUrls.GET_BY_ID)
suspend fun getConversationsById( suspend fun getConvosById(
@FieldMap params: Map<String, String> @FieldMap params: Map<String, String>
): ApiResult<ApiResponse<ConversationsGetByIdResponse>, RestApiError> ): ApiResult<ApiResponse<ConvosGetByIdResponse>, RestApiError>
@FormUrlEncoded @FormUrlEncoded
@POST(ConversationsUrls.DELETE) @POST(ConvosUrls.DELETE)
suspend fun delete( suspend fun delete(
@FieldMap params: Map<String, String> @FieldMap params: Map<String, String>
): ApiResult<ApiResponse<ConversationsDeleteResponse>, RestApiError> ): ApiResult<ApiResponse<ConvosDeleteResponse>, RestApiError>
@FormUrlEncoded @FormUrlEncoded
@POST(ConversationsUrls.PIN) @POST(ConvosUrls.PIN)
suspend fun pin( suspend fun pin(
@FieldMap params: Map<String, String> @FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError> ): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded @FormUrlEncoded
@POST(ConversationsUrls.UNPIN) @POST(ConvosUrls.UNPIN)
suspend fun unpin( suspend fun unpin(
@FieldMap params: Map<String, String> @FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError> ): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded @FormUrlEncoded
@POST(ConversationsUrls.REORDER_PINNED) @POST(ConvosUrls.REORDER_PINNED)
suspend fun reorderPinned( suspend fun reorderPinned(
@FieldMap params: Map<String, String> @FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError> ): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded @FormUrlEncoded
@POST(ConversationsUrls.ARCHIVE) @POST(ConvosUrls.ARCHIVE)
suspend fun archive( suspend fun archive(
@FieldMap params: Map<String, String> @FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError> ): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded @FormUrlEncoded
@POST(ConversationsUrls.UNARCHIVE) @POST(ConvosUrls.UNARCHIVE)
suspend fun unarchive( suspend fun unarchive(
@FieldMap params: Map<String, String> @FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError> ): ApiResult<ApiResponse<Int>, RestApiError>
@@ -1,8 +1,8 @@
package dev.meloda.fast.network.service.conversations package dev.meloda.fast.network.service.convos
import dev.meloda.fast.common.AppConstants import dev.meloda.fast.common.AppConstants
object ConversationsUrls { object ConvosUrls {
private const val URL = AppConstants.URL_API private const val URL = AppConstants.URL_API
@@ -6,7 +6,7 @@ import dev.meloda.fast.model.api.data.VkLongPollData
import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.data.VkMessageData
import dev.meloda.fast.model.api.responses.MessagesCreateChatResponse import dev.meloda.fast.model.api.responses.MessagesCreateChatResponse
import dev.meloda.fast.model.api.responses.MessagesGetByIdResponse import dev.meloda.fast.model.api.responses.MessagesGetByIdResponse
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse import dev.meloda.fast.model.api.responses.MessagesGetConvoMembersResponse
import dev.meloda.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse import dev.meloda.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse
import dev.meloda.fast.model.api.responses.MessagesGetHistoryResponse import dev.meloda.fast.model.api.responses.MessagesGetHistoryResponse
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
@@ -99,10 +99,10 @@ interface MessagesService {
): ApiResult<ApiResponse<VkChatData>, RestApiError> ): ApiResult<ApiResponse<VkChatData>, RestApiError>
@FormUrlEncoded @FormUrlEncoded
@POST(MessagesUrls.GET_CONVERSATIONS_MEMBERS) @POST(MessagesUrls.GET_CONVOS_MEMBERS)
suspend fun getConversationMembers( suspend fun getConvoMembers(
@FieldMap params: Map<String, String> @FieldMap params: Map<String, String>
): ApiResult<ApiResponse<MessagesGetConversationMembersResponse>, RestApiError> ): ApiResult<ApiResponse<MessagesGetConvoMembersResponse>, RestApiError>
@FormUrlEncoded @FormUrlEncoded
@POST(MessagesUrls.REMOVE_CHAT_USER) @POST(MessagesUrls.REMOVE_CHAT_USER)
@@ -18,7 +18,7 @@ object MessagesUrls {
const val GET_BY_ID = "$URL/messages.getById" const val GET_BY_ID = "$URL/messages.getById"
const val MARK_AS_READ = "$URL/messages.markAsRead" const val MARK_AS_READ = "$URL/messages.markAsRead"
const val GET_CHAT = "$URL/messages.getChat" const val GET_CHAT = "$URL/messages.getChat"
const val GET_CONVERSATIONS_MEMBERS = "$URL/messages.getConversationMembers" const val GET_CONVOS_MEMBERS = "$URL/messages.getConversationMembers"
const val REMOVE_CHAT_USER = "$URL/messages.removeChatUser" const val REMOVE_CHAT_USER = "$URL/messages.removeChatUser"
const val GET_HISTORY_ATTACHMENTS = "$URL/messages.getHistoryAttachments" const val GET_HISTORY_ATTACHMENTS = "$URL/messages.getHistoryAttachments"
const val CREATE_CHAT = "$URL/messages.createChat" const val CREATE_CHAT = "$URL/messages.createChat"
@@ -1,4 +1,4 @@
package dev.meloda.fast.ui.model.api package dev.meloda.fast.ui.model.vk
enum class ActionState { enum class ActionState {
PHANTOM, CALL_IN_PROGRESS, NONE; PHANTOM, CALL_IN_PROGRESS, NONE;
@@ -1,41 +1,41 @@
package dev.meloda.fast.ui.model.api package dev.meloda.fast.ui.model.vk
import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.common.model.UiText import dev.meloda.fast.common.model.UiText
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
sealed class ConversationOption( sealed class ConvoOption(
val title: UiText, val title: UiText,
val icon: UiImage val icon: UiImage
) { ) {
data object MarkAsRead : ConversationOption( data object MarkAsRead : ConvoOption(
title = UiText.Resource(R.string.action_mark_as_read), title = UiText.Resource(R.string.action_mark_as_read),
icon = UiImage.Resource(R.drawable.round_done_all_24) icon = UiImage.Resource(R.drawable.round_done_all_24)
) )
data object Pin : ConversationOption( data object Pin : ConvoOption(
title = UiText.Resource(R.string.action_pin), title = UiText.Resource(R.string.action_pin),
icon = UiImage.Resource(R.drawable.pin_outline_24) icon = UiImage.Resource(R.drawable.pin_outline_24)
) )
data object Unpin : ConversationOption( data object Unpin : ConvoOption(
title = UiText.Resource(R.string.action_unpin), title = UiText.Resource(R.string.action_unpin),
icon = UiImage.Resource(R.drawable.pin_off_outline_24) icon = UiImage.Resource(R.drawable.pin_off_outline_24)
) )
data object Delete : ConversationOption( data object Delete : ConvoOption(
title = UiText.Resource(R.string.action_delete), title = UiText.Resource(R.string.action_delete),
icon = UiImage.Resource(R.drawable.round_delete_outline_24) icon = UiImage.Resource(R.drawable.round_delete_outline_24)
) )
data object Archive : ConversationOption( data object Archive : ConvoOption(
title = UiText.Resource(R.string.conversation_context_action_archive), title = UiText.Resource(R.string.convo_context_action_archive),
icon = UiImage.Resource(R.drawable.outline_archive_24) icon = UiImage.Resource(R.drawable.outline_archive_24)
) )
data object Unarchive : ConversationOption( data object Unarchive : ConvoOption(
title = UiText.Resource(R.string.conversation_context_action_unarchive), title = UiText.Resource(R.string.convo_context_action_unarchive),
icon = UiImage.Resource(R.drawable.outline_unarchive_24) icon = UiImage.Resource(R.drawable.outline_unarchive_24)
) )
} }
@@ -0,0 +1,7 @@
package dev.meloda.fast.ui.model.vk
data class MentionIndex(
val id: Long,
val idPrefix: String,
val indexRange: IntRange
)
@@ -1,4 +1,4 @@
package dev.meloda.fast.messageshistory.model package dev.meloda.fast.ui.model.vk
import androidx.compose.runtime.Stable import androidx.compose.runtime.Stable
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
@@ -6,7 +6,8 @@ import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.model.api.domain.VkAttachment import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.ImmutableList
sealed class UiItem( @Stable
sealed class MessageUiItem(
open val id: Long, open val id: Long,
open val cmId: Long open val cmId: Long
) { ) {
@@ -35,8 +36,8 @@ sealed class UiItem(
val attachments: ImmutableList<VkAttachment>?, val attachments: ImmutableList<VkAttachment>?,
val replyCmId: Long?, val replyCmId: Long?,
val replyTitle: String?, val replyTitle: String?,
val replySummary: String? val replySummary: AnnotatedString?
) : UiItem(id, cmId) ) : MessageUiItem(id, cmId)
@Stable @Stable
data class ActionMessage( data class ActionMessage(
@@ -44,5 +45,5 @@ sealed class UiItem(
override val cmId: Long, override val cmId: Long,
val text: AnnotatedString, val text: AnnotatedString,
val actionCmId: Long? val actionCmId: Long?
) : UiItem(id, cmId) ) : MessageUiItem(id, cmId)
} }
@@ -0,0 +1,5 @@
package dev.meloda.fast.ui.model.vk
enum class SendingStatus {
SENDING, SENT, FAILED
}
@@ -1,4 +1,4 @@
package dev.meloda.fast.ui.model.api package dev.meloda.fast.ui.model.vk
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
@@ -8,7 +8,7 @@ import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.ImmutableList
@Immutable @Immutable
data class UiConversation( data class UiConvo(
val id: Long, val id: Long,
val lastMessageId: Long?, val lastMessageId: Long?,
val avatar: UiImage?, val avatar: UiImage?,
@@ -28,5 +28,5 @@ data class UiConversation(
val interactionText: String?, val interactionText: String?,
val isExpanded: Boolean, val isExpanded: Boolean,
val isArchived: Boolean, val isArchived: Boolean,
val options: ImmutableList<ConversationOption>, val options: ImmutableList<ConvoOption>,
) )
@@ -1,4 +1,4 @@
package dev.meloda.fast.ui.model.api package dev.meloda.fast.ui.model.vk
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.common.model.UiImage
@@ -11,6 +11,10 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.input.key.onKeyEvent
@@ -121,3 +125,14 @@ fun isNeedToEnableDarkMode(darkMode: DarkMode): Boolean {
return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && darkMode == DarkMode.FOLLOW_SYSTEM) return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && darkMode == DarkMode.FOLLOW_SYSTEM)
} }
fun Color.lighten(amount: Float) = lerp(this, Color.White, amount.coerceIn(0f, 1f))
fun Color.darken(amount: Float) = lerp(this, Color.Black, amount.coerceIn(0f, 1f))
fun Color.isDark(
background: Color = Color.White,
threshold: Float = 0.5f
): Boolean {
val opaque = if (alpha < 1f) this.compositeOver(background) else this
return opaque.luminance() < threshold
}
+11 -11
View File
@@ -24,16 +24,16 @@
<string name="message_mark_as_spam">Пометить как спам</string> <string name="message_mark_as_spam">Пометить как спам</string>
<string name="action_mark_as_read">Прочитать</string> <string name="action_mark_as_read">Прочитать</string>
<string name="action_delete">Удалить</string> <string name="action_delete">Удалить</string>
<string name="conversation_context_action_unarchive">Из архива</string> <string name="convo_context_action_unarchive">Из архива</string>
<string name="conversation_context_action_delete">Удалить</string> <string name="convo_context_action_delete">Удалить</string>
<string name="confirm_delete_conversation">Удалить чат?</string> <string name="confirm_delete_convo">Удалить чат?</string>
<string name="action_sign_out">Выйти</string> <string name="action_sign_out">Выйти</string>
<string name="sign_out_confirm_title">Выйти?</string> <string name="sign_out_confirm_title">Выйти?</string>
<string name="conversation_context_action_unpin">Открепить</string> <string name="convo_context_action_unpin">Открепить</string>
<string name="conversation_context_action_pin">Закрепить</string> <string name="convo_context_action_pin">Закрепить</string>
<string name="confirm_unpin_conversation">Открепить чат?</string> <string name="confirm_unpin_convo">Открепить чат?</string>
<string name="confirm_pin_conversation">Закрепить чат?</string> <string name="confirm_pin_convo">Закрепить чат?</string>
<string name="confirm_unarchive_conversation">Разархивировать чат?</string> <string name="confirm_unarchive_convo">Разархивировать чат?</string>
<string name="action_pin">Закрепить</string> <string name="action_pin">Закрепить</string>
<string name="action_unpin">Открепить</string> <string name="action_unpin">Открепить</string>
<string name="action_mark">Пометить</string> <string name="action_mark">Пометить</string>
@@ -181,7 +181,7 @@
<string name="settings_amoled_dark_theme_description">Использовать AMOLED тему с чистым чёрным фоновым цветом, когда используется тёмная тема</string> <string name="settings_amoled_dark_theme_description">Использовать AMOLED тему с чистым чёрным фоновым цветом, когда используется тёмная тема</string>
<string name="members_count">Участники: %1$d</string> <string name="members_count">Участники: %1$d</string>
<string name="title_loading">Загрузка&#8230;</string> <string name="title_loading">Загрузка&#8230;</string>
<string name="title_conversations">Чаты</string> <string name="title_convos">Чаты</string>
<string name="title_archive">Архив</string> <string name="title_archive">Архив</string>
<string name="title_friends">Друзья</string> <string name="title_friends">Друзья</string>
<string name="title_profile">Профиль</string> <string name="title_profile">Профиль</string>
@@ -262,8 +262,8 @@
<string name="unspam_message_text">Вы уверены, что хотите убрать пометку спама у этого сообщения?</string> <string name="unspam_message_text">Вы уверены, что хотите убрать пометку спама у этого сообщения?</string>
<string name="pin_message_title">Закрепить сообщение</string> <string name="pin_message_title">Закрепить сообщение</string>
<string name="copied_to_clipboard">Скопировано в буфер обмена</string> <string name="copied_to_clipboard">Скопировано в буфер обмена</string>
<string name="conversation_context_action_archive">В архив</string> <string name="convo_context_action_archive">В архив</string>
<string name="confirm_archive_conversation">Архивировать чат?</string> <string name="confirm_archive_convo">Архивировать чат?</string>
<string name="action_archive">В архив</string> <string name="action_archive">В архив</string>
<string name="autofill">Автозаполнение</string> <string name="autofill">Автозаполнение</string>
<string name="bold">Жирный</string> <string name="bold">Жирный</string>
+11 -11
View File
@@ -153,18 +153,18 @@
<string name="action_mark_as_read">Read</string> <string name="action_mark_as_read">Read</string>
<string name="action_delete">Delete</string> <string name="action_delete">Delete</string>
<string name="conversation_context_action_archive">Archive</string> <string name="convo_context_action_archive">Archive</string>
<string name="conversation_context_action_unarchive">Unarchive</string> <string name="convo_context_action_unarchive">Unarchive</string>
<string name="conversation_context_action_delete">Delete</string> <string name="convo_context_action_delete">Delete</string>
<string name="confirm_delete_conversation">Delete the conversation?</string> <string name="confirm_delete_convo">Delete the conversation?</string>
<string name="action_sign_out">Sign out</string> <string name="action_sign_out">Sign out</string>
<string name="sign_out_confirm_title">Sign out?</string> <string name="sign_out_confirm_title">Sign out?</string>
<string name="conversation_context_action_unpin">Unpin</string> <string name="convo_context_action_unpin">Unpin</string>
<string name="conversation_context_action_pin">Pin</string> <string name="convo_context_action_pin">Pin</string>
<string name="confirm_unpin_conversation">Unpin the conversation?</string> <string name="confirm_unpin_convo">Unpin the conversation?</string>
<string name="confirm_pin_conversation">Pin the conversation?</string> <string name="confirm_pin_convo">Pin the conversation?</string>
<string name="confirm_archive_conversation">Archive the conversation?</string> <string name="confirm_archive_convo">Archive the conversation?</string>
<string name="confirm_unarchive_conversation">Unarchive the conversation?</string> <string name="confirm_unarchive_convo">Unarchive the conversation?</string>
<string name="action_pin">Pin</string> <string name="action_pin">Pin</string>
<string name="action_unpin">Unpin</string> <string name="action_unpin">Unpin</string>
<string name="action_mark">Mark</string> <string name="action_mark">Mark</string>
@@ -245,7 +245,7 @@
<string name="action_refresh">Refresh</string> <string name="action_refresh">Refresh</string>
<string name="members_count">Members: %1$d</string> <string name="members_count">Members: %1$d</string>
<string name="title_loading">Loading&#8230;</string> <string name="title_loading">Loading&#8230;</string>
<string name="title_conversations">Conversations</string> <string name="title_convos">Conversations</string>
<string name="title_archive">Archive</string> <string name="title_archive">Archive</string>
<string name="title_friends">Friends</string> <string name="title_friends">Friends</string>
<string name="title_profile">Profile</string> <string name="title_profile">Profile</string>
@@ -53,7 +53,7 @@ class ChatMaterialsViewModelImpl(
screenState.setValue { old -> screenState.setValue { old ->
old.copy( old.copy(
peerId = arguments.peerId, peerId = arguments.peerId,
cmId = arguments.conversationMessageId cmId = arguments.cmId
) )
} }
@@ -101,7 +101,7 @@ class ChatMaterialsViewModelImpl(
isPaginationExhausted = paginationExhausted, isPaginationExhausted = paginationExhausted,
cmId = if (loadedMaterials.size + offset > 200) { cmId = if (loadedMaterials.size + offset > 200) {
currentOffset.setValue { 0 } currentOffset.setValue { 0 }
loadedMaterials.lastOrNull()?.conversationMessageId ?: -1 loadedMaterials.lastOrNull()?.cmId ?: -1
} else { } else {
screenState.value.cmId screenState.value.cmId
} }
@@ -1,43 +1,43 @@
package dev.meloda.fast.chatmaterials.model package dev.meloda.fast.chatmaterials.model
sealed class UiChatMaterial( sealed class UiChatMaterial(
open val conversationMessageId: Long open val cmId: Long
) { ) {
data class Photo( data class Photo(
override val conversationMessageId: Long, override val cmId: Long,
val previewUrl: String val previewUrl: String
) : UiChatMaterial(conversationMessageId) ) : UiChatMaterial(cmId)
data class Video( data class Video(
override val conversationMessageId: Long, override val cmId: Long,
val previewUrl: String?, val previewUrl: String?,
val title: String, val title: String,
val views: Int, val views: Int,
val duration: String val duration: String
) : UiChatMaterial(conversationMessageId) ) : UiChatMaterial(cmId)
data class Audio( data class Audio(
override val conversationMessageId: Long, override val cmId: Long,
val previewUrl: String?, val previewUrl: String?,
val title: String, val title: String,
val artist: String, val artist: String,
val duration: String val duration: String
) : UiChatMaterial(conversationMessageId) ) : UiChatMaterial(cmId)
data class File( data class File(
override val conversationMessageId: Long, override val cmId: Long,
val previewUrl: String?, val previewUrl: String?,
val title: String, val title: String,
val size: String, val size: String,
val extension: String val extension: String
) : UiChatMaterial(conversationMessageId) ) : UiChatMaterial(cmId)
data class Link( data class Link(
override val conversationMessageId: Long, override val cmId: Long,
val previewUrl: String?, val previewUrl: String?,
val title: String?, val title: String?,
val url: String, val url: String,
val urlFirstChar: String val urlFirstChar: String
) : UiChatMaterial(conversationMessageId) ) : UiChatMaterial(cmId)
} }
@@ -11,7 +11,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class ChatMaterials( data class ChatMaterials(
val peerId: Long, val peerId: Long,
val conversationMessageId: Long val cmId: Long
) { ) {
companion object { companion object {
fun from(savedStateHandle: SavedStateHandle) = fun from(savedStateHandle: SavedStateHandle) =
@@ -31,11 +31,11 @@ fun NavGraphBuilder.chatMaterialsScreen(
} }
} }
fun NavController.navigateToChatMaterials(peerId: Long, conversationMessageId: Long) { fun NavController.navigateToChatMaterials(peerId: Long, cmId: Long) {
this.navigate( this.navigate(
ChatMaterials( ChatMaterials(
peerId = peerId, peerId = peerId,
conversationMessageId = conversationMessageId cmId = cmId
) )
) )
} }
@@ -17,7 +17,7 @@ fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial? =
AttachmentType.PHOTO -> { AttachmentType.PHOTO -> {
val attachment = this.attachment as VkPhotoDomain val attachment = this.attachment as VkPhotoDomain
UiChatMaterial.Photo( UiChatMaterial.Photo(
conversationMessageId = this.conversationMessageId, cmId = this.cmId,
previewUrl = attachment.getSizeOrSmaller(VkPhotoDomain.SIZE_TYPE_1080_1024)?.url.orEmpty() previewUrl = attachment.getSizeOrSmaller(VkPhotoDomain.SIZE_TYPE_1080_1024)?.url.orEmpty()
) )
} }
@@ -47,7 +47,7 @@ fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial? =
builder.toString().format(Locale.getDefault(), *args.toTypedArray()) builder.toString().format(Locale.getDefault(), *args.toTypedArray())
UiChatMaterial.Video( UiChatMaterial.Video(
conversationMessageId = this.conversationMessageId, cmId = this.cmId,
previewUrl = attachment.images.maxByOrNull(VkVideoDomain.VideoImage::width)?.url.orEmpty(), previewUrl = attachment.images.maxByOrNull(VkVideoDomain.VideoImage::width)?.url.orEmpty(),
title = attachment.title, title = attachment.title,
views = attachment.views, views = attachment.views,
@@ -80,7 +80,7 @@ fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial? =
builder.toString().format(Locale.getDefault(), *args.toTypedArray()) builder.toString().format(Locale.getDefault(), *args.toTypedArray())
UiChatMaterial.Audio( UiChatMaterial.Audio(
conversationMessageId = this.conversationMessageId, cmId = this.cmId,
previewUrl = null, previewUrl = null,
title = attachment.title, title = attachment.title,
artist = attachment.artist, artist = attachment.artist,
@@ -112,7 +112,7 @@ fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial? =
} }
UiChatMaterial.File( UiChatMaterial.File(
conversationMessageId = this.conversationMessageId, cmId = this.cmId,
title = attachment.title, title = attachment.title,
previewUrl = previewUrl, previewUrl = previewUrl,
size = AndroidUtils.bytesToHumanReadableSize(attachment.size.toDouble()), size = AndroidUtils.bytesToHumanReadableSize(attachment.size.toDouble()),
@@ -124,7 +124,7 @@ fun VkAttachmentHistoryMessage.asPresentation(): UiChatMaterial? =
val attachment = this.attachment as VkLinkDomain val attachment = this.attachment as VkLinkDomain
UiChatMaterial.Link( UiChatMaterial.Link(
conversationMessageId = this.conversationMessageId, cmId = this.cmId,
title = attachment.title, title = attachment.title,
previewUrl = attachment.photo?.getMaxSize()?.url, previewUrl = attachment.photo?.getMaxSize()?.url,
url = attachment.url, url = attachment.url,
@@ -1,746 +0,0 @@
package dev.meloda.fast.conversations
import android.content.Context
import android.content.res.Resources
import android.os.Bundle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import coil.ImageLoader
import coil.request.ImageRequest
import com.conena.nanokt.collections.indexOfFirstOrNull
import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.createTimerFlow
import dev.meloda.fast.common.extensions.findWithIndex
import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.extensions.updateValue
import dev.meloda.fast.conversations.model.ConversationDialog
import dev.meloda.fast.conversations.model.ConversationNavigation
import dev.meloda.fast.conversations.model.ConversationsScreenState
import dev.meloda.fast.conversations.model.InteractionJob
import dev.meloda.fast.conversations.model.NewInteractionException
import dev.meloda.fast.conversations.util.asPresentation
import dev.meloda.fast.conversations.util.extractAvatar
import dev.meloda.fast.data.VkUtils
import dev.meloda.fast.data.processState
import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.domain.ConversationsUseCase
import dev.meloda.fast.domain.LoadConversationsByIdUseCase
import dev.meloda.fast.domain.LongPollUpdatesParser
import dev.meloda.fast.domain.MessagesUseCase
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.ConversationsFilter
import dev.meloda.fast.model.InteractionType
import dev.meloda.fast.model.LongPollParsedEvent
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.ui.model.api.ConversationOption
import dev.meloda.fast.ui.model.api.UiConversation
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
class ConversationsViewModel(
updatesParser: LongPollUpdatesParser,
private val filter: ConversationsFilter,
private val conversationsUseCase: ConversationsUseCase,
private val messagesUseCase: MessagesUseCase,
private val resources: Resources,
private val userSettings: UserSettings,
private val imageLoader: ImageLoader,
private val applicationContext: Context,
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase
) : ViewModel() {
private val _screenState = MutableStateFlow(ConversationsScreenState.EMPTY)
val screenState = _screenState.asStateFlow()
private val _navigation = MutableStateFlow<ConversationNavigation?>(null)
val navigation = _navigation.asStateFlow()
private val _dialog = MutableStateFlow<ConversationDialog?>(null)
val dialog = _dialog.asStateFlow()
private val _conversations = MutableStateFlow<List<VkConversation>>(emptyList())
val conversations = _conversations.asStateFlow()
private val _uiConversations = MutableStateFlow<List<UiConversation>>(emptyList())
val uiConversations = _uiConversations.asStateFlow()
private val pinnedConversationsCount = conversations.map { conversations ->
conversations.count(VkConversation::isPinned)
}.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
private val _baseError = MutableStateFlow<BaseError?>(null)
val baseError = _baseError.asStateFlow()
private val _currentOffset = MutableStateFlow(0)
val currentOffset = _currentOffset.asStateFlow()
private val _canPaginate = MutableStateFlow(false)
val canPaginate = _canPaginate.asStateFlow()
private val expandedConversationId = MutableStateFlow(0L)
private val useContactNames: Boolean get() = userSettings.useContactNames.value
private val interactionsTimers = hashMapOf<Long, InteractionJob?>()
init {
_screenState.updateValue { copy(isArchive = filter == ConversationsFilter.ARCHIVE) }
loadConversations()
updatesParser.onNewMessage(::handleNewMessage)
updatesParser.onMessageEdited(::handleEditedMessage)
updatesParser.onMessageIncomingRead(::handleReadIncomingMessage)
updatesParser.onMessageOutgoingRead(::handleReadOutgoingMessage)
updatesParser.onInteractions(::handleInteraction)
updatesParser.onChatMajorChanged(::handleChatMajorChanged)
updatesParser.onChatMinorChanged(::handleChatMinorChanged)
updatesParser.onChatCleared(::handleChatClearing)
updatesParser.onChatArchived(::handleChatArchived)
userSettings.useContactNames.listenValue(viewModelScope) {
syncUiConversation()
}
}
fun onNavigationConsumed() {
_navigation.setValue { null }
}
fun onDialogConfirmed(dialog: ConversationDialog, bundle: Bundle) {
onDialogDismissed(dialog)
when (dialog) {
is ConversationDialog.ConversationDelete -> {
deleteConversation(dialog.conversationId)
}
is ConversationDialog.ConversationPin -> {
pinConversation(dialog.conversationId, true)
}
is ConversationDialog.ConversationUnpin -> {
pinConversation(dialog.conversationId, false)
}
is ConversationDialog.ConversationArchive -> {
archiveConversation(dialog.conversationId, true)
}
is ConversationDialog.ConversationUnarchive -> {
archiveConversation(dialog.conversationId, false)
}
}
expandedConversationId.setValue { 0 }
syncUiConversation()
}
fun onDialogDismissed(dialog: ConversationDialog) {
_dialog.setValue { null }
}
fun onDialogItemPicked(dialog: ConversationDialog, bundle: Bundle) {
when (dialog) {
is ConversationDialog.ConversationDelete -> Unit
is ConversationDialog.ConversationPin -> Unit
is ConversationDialog.ConversationUnpin -> Unit
is ConversationDialog.ConversationArchive -> Unit
is ConversationDialog.ConversationUnarchive -> Unit
}
}
fun onErrorButtonClicked() {
when (baseError.value) {
null -> Unit
is BaseError.ConnectionError,
is BaseError.InternalError,
is BaseError.SimpleError,
is BaseError.UnknownError -> onRefresh()
else -> Unit
}
}
fun onPaginationConditionsMet() {
_currentOffset.update { conversations.value.size }
loadConversations()
}
fun onRefresh() {
onErrorConsumed()
loadConversations(offset = 0)
}
fun onConversationItemClick(conversation: UiConversation) {
collapseConversations()
_navigation.setValue { ConversationNavigation.MessagesHistory(peerId = conversation.id) }
}
fun onConversationItemLongClick(conversation: UiConversation) {
expandedConversationId.setValue {
if (conversation.isExpanded) 0
else conversation.id
}
syncUiConversation()
}
fun onOptionClicked(
conversation: UiConversation,
option: ConversationOption
) {
when (option) {
ConversationOption.Delete -> {
_dialog.setValue { ConversationDialog.ConversationDelete(conversation.id) }
}
ConversationOption.MarkAsRead -> {
conversation.lastMessageId?.let { lastMessageId ->
readConversation(
peerId = conversation.id,
startMessageId = lastMessageId
)
collapseConversations()
}
}
ConversationOption.Pin -> {
_dialog.setValue { ConversationDialog.ConversationPin(conversation.id) }
}
ConversationOption.Unpin -> {
_dialog.setValue { ConversationDialog.ConversationUnpin(conversation.id) }
}
ConversationOption.Archive -> {
_dialog.setValue { ConversationDialog.ConversationArchive(conversation.id) }
}
ConversationOption.Unarchive -> {
_dialog.setValue { ConversationDialog.ConversationUnarchive(conversation.id) }
}
}
}
fun onErrorConsumed() {
_baseError.setValue { null }
}
fun setScrollIndex(index: Int) {
_screenState.setValue { old -> old.copy(scrollIndex = index) }
}
fun setScrollOffset(offset: Int) {
_screenState.setValue { old -> old.copy(scrollOffset = offset) }
}
fun onCreateChatButtonClicked() {
_navigation.setValue { ConversationNavigation.CreateChat }
}
private fun collapseConversations() {
expandedConversationId.setValue { 0 }
syncUiConversation()
}
private fun loadConversations(
offset: Int = currentOffset.value
) {
conversationsUseCase.getConversations(
count = LOAD_COUNT,
offset = offset,
filter = filter
).listenValue(viewModelScope) { state ->
state.processState(
error = { error ->
val newBaseError = VkUtils.parseError(error)
_baseError.update { newBaseError }
},
success = { response ->
val conversations = response
val fullConversations = if (offset == 0) {
conversations
} else {
this.conversations.value.plus(conversations)
}
val itemsCountSufficient = response.size == LOAD_COUNT
val paginationExhausted = !itemsCountSufficient &&
this.conversations.value.isNotEmpty()
_screenState.updateValue {
copy(isPaginationExhausted = paginationExhausted)
}
val imagesToPreload =
response.mapNotNull { it.extractAvatar().extractUrl() }
imagesToPreload.forEach { url ->
imageLoader.enqueue(
ImageRequest.Builder(applicationContext)
.data(url)
.build()
)
}
conversationsUseCase.storeConversations(response)
_conversations.emit(fullConversations)
syncUiConversation()
_canPaginate.setValue { itemsCountSufficient }
}
)
_screenState.setValue { old ->
old.copy(
isLoading = offset == 0 && state.isLoading(),
isPaginating = offset > 0 && state.isLoading()
)
}
}
}
private fun deleteConversation(peerId: Long) {
conversationsUseCase.delete(peerId).listenValue(viewModelScope) { state ->
state.processState(
error = {},
success = {
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == peerId }
?: return@processState
newConversations.removeAt(conversationIndex)
_conversations.update { newConversations.sorted() }
syncUiConversation()
}
)
_screenState.emit(screenState.value.copy(isLoading = state.isLoading()))
}
}
private fun pinConversation(peerId: Long, pin: Boolean) {
conversationsUseCase.changePinState(peerId, pin)
.listenValue(viewModelScope) { state ->
state.processState(
error = {},
success = {
handleChatMajorChanged(
LongPollParsedEvent.ChatMajorChanged(
peerId = peerId,
majorId = if (pin) {
pinnedConversationsCount.value.plus(1) * 16
} else {
0
}
)
)
}
)
_screenState.setValue { old -> old.copy(isLoading = state.isLoading()) }
}
}
private fun archiveConversation(peerId: Long, archive: Boolean) {
conversationsUseCase.changeArchivedState(peerId, archive)
.listenValue(viewModelScope) { state ->
state.processState(
error = {},
success = {
conversations.value.find { it.id == peerId }?.let { conversation ->
handleChatArchived(
LongPollParsedEvent.ChatArchived(
conversation = conversation,
archived = archive
)
)
}
}
)
}
}
// TODO: 03-Apr-25, Danil Nikolaev: handle business messages
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
val message = event.message
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == message.peerId }
if (conversationIndex == null) {
if (event.inArchive != (filter == ConversationsFilter.ARCHIVE)) return
loadConversationsByIdUseCase(
peerIds = listOf(message.peerId),
extended = true,
fields = VkConstants.ALL_FIELDS
).listenValue(viewModelScope) { state ->
state.processState(
error = {},
success = { response ->
val conversation = (response.firstOrNull() ?: return@listenValue)
.copy(lastMessage = message)
newConversations.add(pinnedConversationsCount.value, conversation)
_conversations.update { newConversations.sorted() }
syncUiConversation()
}
)
}
} else {
val conversation = newConversations[conversationIndex]
var newConversation = conversation.copy(
lastMessage = message,
lastMessageId = message.id,
lastCmId = message.cmId,
unreadCount = if (message.isOut) conversation.unreadCount
else conversation.unreadCount + 1
)
interactionsTimers[conversation.id]?.let { job ->
if (job.interactionType == InteractionType.Typing
&& message.fromId in conversation.interactionIds
) {
val newInteractionIds = newConversation.interactionIds.filter { id ->
id != message.fromId
}
newConversation = newConversation.copy(
interactionType = if (newInteractionIds.isEmpty()) -1 else {
newConversation.interactionType
},
interactionIds = newInteractionIds
)
}
}
if (conversation.isPinned()) {
newConversations[conversationIndex] = newConversation
} else {
newConversations.removeAt(conversationIndex)
val toPosition = pinnedConversationsCount.value
newConversations.add(toPosition, newConversation)
}
_conversations.update { newConversations.sorted() }
syncUiConversation()
}
}
private fun handleEditedMessage(event: LongPollParsedEvent.MessageEdited) {
val message = event.message
val newConversations = conversations.value.toMutableList()
val conversationIndex = newConversations.indexOfFirstOrNull { it.id == message.peerId }
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
val conversation = newConversations[conversationIndex]
newConversations[conversationIndex] = conversation.copy(
lastMessage = message,
lastMessageId = message.id,
lastCmId = message.cmId
)
_conversations.update { newConversations }
syncUiConversation()
}
}
private fun handleReadIncomingMessage(event: LongPollParsedEvent.IncomingMessageRead) {
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == event.peerId }
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations[conversationIndex] =
newConversations[conversationIndex].copy(
inReadCmId = event.cmId,
unreadCount = event.unreadCount
)
_conversations.update { newConversations }
syncUiConversation()
}
}
private fun handleReadOutgoingMessage(event: LongPollParsedEvent.OutgoingMessageRead) {
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == event.peerId }
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations[conversationIndex] =
newConversations[conversationIndex].copy(
outReadCmId = event.cmId,
unreadCount = event.unreadCount
)
_conversations.update { newConversations }
syncUiConversation()
}
}
private fun handleInteraction(event: LongPollParsedEvent.Interaction) {
val interactionType = event.interactionType
val peerId = event.peerId
val userIds = event.userIds
val newConversations = conversations.value.toMutableList()
val conversationAndIndex =
newConversations.findWithIndex { it.id == peerId }
if (conversationAndIndex != null) {
newConversations[conversationAndIndex.first] =
conversationAndIndex.second.copy(
interactionType = interactionType.value,
interactionIds = userIds
)
_conversations.update { newConversations }
syncUiConversation()
interactionsTimers[peerId]?.let { interactionJob ->
if (interactionJob.interactionType == interactionType) {
interactionJob.timerJob.cancel(NewInteractionException())
}
}
var timeoutAction: (() -> Unit)? = null
val timerJob = createTimerFlow(
time = 6,
onTimeoutAction = { timeoutAction?.invoke() }
).launchIn(viewModelScope)
val newInteractionJob = InteractionJob(
interactionType = interactionType,
timerJob = timerJob
)
interactionsTimers[peerId] = newInteractionJob
timeoutAction = {
stopInteraction(peerId, newInteractionJob)
}
}
}
private fun stopInteraction(peerId: Long, interactionJob: InteractionJob) {
interactionsTimers[peerId] ?: return
val newConversations = conversations.value.toMutableList()
val conversationAndIndex =
newConversations.findWithIndex { it.id == peerId } ?: return
newConversations[conversationAndIndex.first] =
conversationAndIndex.second.copy(
interactionType = -1,
interactionIds = emptyList()
)
_conversations.update { newConversations }
syncUiConversation()
interactionJob.timerJob.cancel()
interactionsTimers[peerId] = null
}
private fun handleChatMajorChanged(event: LongPollParsedEvent.ChatMajorChanged) {
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == event.peerId }
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations[conversationIndex] =
newConversations[conversationIndex].copy(majorId = event.majorId)
_conversations.setValue { newConversations.sorted() }
syncUiConversation()
}
}
private fun handleChatMinorChanged(event: LongPollParsedEvent.ChatMinorChanged) {
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == event.peerId }
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations[conversationIndex] =
newConversations[conversationIndex].copy(minorId = event.minorId)
_conversations.setValue { newConversations.sorted() }
syncUiConversation()
}
}
private fun handleChatClearing(event: LongPollParsedEvent.ChatCleared) {
val newConversations = conversations.value.toMutableList()
val conversationIndex = newConversations.indexOfFirstOrNull { it.id == event.peerId }
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations.removeAt(conversationIndex)
_conversations.setValue { newConversations.sorted() }
syncUiConversation()
}
}
private fun handleChatArchived(event: LongPollParsedEvent.ChatArchived) {
val conversation = event.conversation
val newConversations = conversations.value.toMutableList()
when (filter) {
ConversationsFilter.BUSINESS_NOTIFY -> Unit
ConversationsFilter.ARCHIVE -> {
if (event.archived) {
newConversations.add(0, conversation)
} else {
val index = newConversations.indexOfFirstOrNull { it.id == conversation.id }
if (index == null) return
newConversations.removeAt(index)
}
_conversations.update { newConversations }
syncUiConversation()
}
else -> {
if (event.archived) {
val index = newConversations.indexOfFirstOrNull { it.id == conversation.id }
if (index == null) return
newConversations.removeAt(index)
} else {
newConversations.add(pinnedConversationsCount.value, conversation)
}
_conversations.update { newConversations.sorted() }
syncUiConversation()
}
}
}
private fun readConversation(peerId: Long, startMessageId: Long) {
messagesUseCase.markAsRead(
peerId = peerId,
startMessageId = startMessageId
).listenValue(viewModelScope) { state ->
state.processState(
error = {},
success = {
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == peerId }
?: return@listenValue
newConversations[conversationIndex] =
newConversations[conversationIndex].copy(inRead = startMessageId)
_conversations.update { newConversations }
syncUiConversation()
}
)
}
}
private fun List<VkConversation>.sorted(): List<VkConversation> {
val newConversations = toMutableList()
val pinnedConversations = newConversations
.filter(VkConversation::isPinned)
.sortedWith { c1, c2 ->
val diff = c2.majorId - c1.majorId
if (diff == 0) {
c2.minorId - c1.minorId
} else {
diff
}
}
newConversations.removeAll(pinnedConversations)
newConversations.sortWith { c1, c2 ->
(c2.lastMessage?.date ?: 0) - (c1.lastMessage?.date ?: 0)
}
newConversations.addAll(0, pinnedConversations)
return newConversations
}
private fun syncUiConversation(): List<UiConversation> {
val conversations = conversations.value
val newUiConversations = conversations.map { conversation ->
val options = mutableListOf<ConversationOption>()
conversation.lastMessage?.run {
if (!conversation.isRead() && !this.isOut) {
options += ConversationOption.MarkAsRead
}
}
val conversationsSize = this.conversations.value.size
val pinnedCount = pinnedConversationsCount.value
val canPinOneMoreDialog =
conversationsSize > 4 && pinnedCount < 5 && !conversation.isPinned()
if (conversation.isPinned()) {
options += ConversationOption.Unpin
} else if (canPinOneMoreDialog) {
options += ConversationOption.Pin
}
when (filter) {
ConversationsFilter.ARCHIVE -> ConversationOption.Unarchive
ConversationsFilter.UNREAD,
ConversationsFilter.ALL -> ConversationOption.Archive
ConversationsFilter.BUSINESS_NOTIFY -> null
}?.let(options::add)
options += ConversationOption.Delete
conversation.asPresentation(
resources = resources,
useContactName = useContactNames,
isExpanded = expandedConversationId.value == conversation.id,
options = options.toImmutableList()
)
}
_uiConversations.setValue { newUiConversations }
return newUiConversations
}
companion object {
const val LOAD_COUNT = 30
}
}
@@ -1,37 +0,0 @@
package dev.meloda.fast.conversations.di
import dev.meloda.fast.conversations.ConversationsViewModel
import dev.meloda.fast.domain.ConversationsUseCase
import dev.meloda.fast.domain.ConversationsUseCaseImpl
import dev.meloda.fast.model.ConversationsFilter
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.core.scope.Scope
import org.koin.dsl.bind
import org.koin.dsl.module
val conversationsModule = module {
viewModel(named(ConversationsFilter.ALL)) {
createConversationsViewModel(ConversationsFilter.ALL)
}
viewModel(named(ConversationsFilter.ARCHIVE)) {
createConversationsViewModel(ConversationsFilter.ARCHIVE)
}
singleOf(::ConversationsUseCaseImpl) bind ConversationsUseCase::class
}
private fun Scope.createConversationsViewModel(filter: ConversationsFilter): ConversationsViewModel {
return ConversationsViewModel(
filter = filter,
updatesParser = get(),
conversationsUseCase = get(),
messagesUseCase = get(),
resources = get(),
userSettings = get(),
imageLoader = get(),
applicationContext = get(),
loadConversationsByIdUseCase = get()
)
}
@@ -1,12 +0,0 @@
package dev.meloda.fast.conversations.model
import androidx.compose.runtime.Immutable
@Immutable
sealed class ConversationDialog {
data class ConversationPin(val conversationId: Long) : ConversationDialog()
data class ConversationUnpin(val conversationId: Long) : ConversationDialog()
data class ConversationDelete(val conversationId: Long) : ConversationDialog()
data class ConversationArchive(val conversationId: Long) : ConversationDialog()
data class ConversationUnarchive(val conversationId: Long) : ConversationDialog()
}
@@ -1,11 +0,0 @@
package dev.meloda.fast.conversations.model
import androidx.compose.runtime.Immutable
@Immutable
sealed class ConversationNavigation {
data class MessagesHistory(val peerId: Long) : ConversationNavigation()
data object CreateChat : ConversationNavigation()
}
@@ -4,7 +4,7 @@ plugins {
} }
android { android {
namespace = "dev.meloda.fast.conversations" namespace = "dev.meloda.fast.convos"
} }
dependencies { dependencies {
@@ -0,0 +1,746 @@
package dev.meloda.fast.convos
import android.content.Context
import android.content.res.Resources
import android.os.Bundle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import coil.ImageLoader
import coil.request.ImageRequest
import com.conena.nanokt.collections.indexOfFirstOrNull
import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.common.extensions.createTimerFlow
import dev.meloda.fast.common.extensions.findWithIndex
import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.common.extensions.updateValue
import dev.meloda.fast.convos.model.ConvoDialog
import dev.meloda.fast.convos.model.ConvoNavigation
import dev.meloda.fast.convos.model.ConvosScreenState
import dev.meloda.fast.convos.model.InteractionJob
import dev.meloda.fast.convos.model.NewInteractionException
import dev.meloda.fast.data.VkUtils
import dev.meloda.fast.data.processState
import dev.meloda.fast.datastore.UserSettings
import dev.meloda.fast.domain.ConvoUseCase
import dev.meloda.fast.domain.LoadConvosByIdUseCase
import dev.meloda.fast.domain.LongPollUpdatesParser
import dev.meloda.fast.domain.MessagesUseCase
import dev.meloda.fast.domain.util.asPresentation
import dev.meloda.fast.domain.util.extractAvatar
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.InteractionType
import dev.meloda.fast.model.LongPollParsedEvent
import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.ui.model.vk.ConvoOption
import dev.meloda.fast.ui.model.vk.UiConvo
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
class ConvosViewModel(
updatesParser: LongPollUpdatesParser,
private val filter: ConvosFilter,
private val convoUseCase: ConvoUseCase,
private val messagesUseCase: MessagesUseCase,
private val resources: Resources,
private val userSettings: UserSettings,
private val imageLoader: ImageLoader,
private val applicationContext: Context,
private val loadConvosByIdUseCase: LoadConvosByIdUseCase
) : ViewModel() {
private val _screenState = MutableStateFlow(ConvosScreenState.EMPTY)
val screenState = _screenState.asStateFlow()
private val _navigation = MutableStateFlow<ConvoNavigation?>(null)
val navigation = _navigation.asStateFlow()
private val _dialog = MutableStateFlow<ConvoDialog?>(null)
val dialog = _dialog.asStateFlow()
private val _convos = MutableStateFlow<List<VkConvo>>(emptyList())
val convos = _convos.asStateFlow()
private val _uiConvos = MutableStateFlow<List<UiConvo>>(emptyList())
val uiConvos = _uiConvos.asStateFlow()
private val pinnedConvosCount = convos.map { convos ->
convos.count(VkConvo::isPinned)
}.stateIn(viewModelScope, SharingStarted.Eagerly, 0)
private val _baseError = MutableStateFlow<BaseError?>(null)
val baseError = _baseError.asStateFlow()
private val _currentOffset = MutableStateFlow(0)
val currentOffset = _currentOffset.asStateFlow()
private val _canPaginate = MutableStateFlow(false)
val canPaginate = _canPaginate.asStateFlow()
private val expandedConvoId = MutableStateFlow(0L)
private val useContactNames: Boolean get() = userSettings.useContactNames.value
private val interactionsTimers = hashMapOf<Long, InteractionJob?>()
init {
_screenState.updateValue { copy(isArchive = filter == ConvosFilter.ARCHIVE) }
loadConvos()
updatesParser.onNewMessage(::handleNewMessage)
updatesParser.onMessageEdited(::handleEditedMessage)
updatesParser.onMessageIncomingRead(::handleReadIncomingMessage)
updatesParser.onMessageOutgoingRead(::handleReadOutgoingMessage)
updatesParser.onInteractions(::handleInteraction)
updatesParser.onChatMajorChanged(::handleChatMajorChanged)
updatesParser.onChatMinorChanged(::handleChatMinorChanged)
updatesParser.onChatCleared(::handleChatClearing)
updatesParser.onChatArchived(::handleChatArchived)
userSettings.useContactNames.listenValue(viewModelScope) {
syncUiConvos()
}
}
fun onNavigationConsumed() {
_navigation.setValue { null }
}
fun onDialogConfirmed(dialog: ConvoDialog, bundle: Bundle) {
onDialogDismissed(dialog)
when (dialog) {
is ConvoDialog.ConvoDelete -> {
deleteConvo(dialog.convoId)
}
is ConvoDialog.ConvoPin -> {
pinConvo(dialog.convoId, true)
}
is ConvoDialog.ConvoUnpin -> {
pinConvo(dialog.convoId, false)
}
is ConvoDialog.ConvoArchive -> {
archiveConvo(dialog.convoId, true)
}
is ConvoDialog.ConvoUnarchive -> {
archiveConvo(dialog.convoId, false)
}
}
expandedConvoId.setValue { 0 }
syncUiConvos()
}
fun onDialogDismissed(dialog: ConvoDialog) {
_dialog.setValue { null }
}
fun onDialogItemPicked(dialog: ConvoDialog, bundle: Bundle) {
when (dialog) {
is ConvoDialog.ConvoDelete -> Unit
is ConvoDialog.ConvoPin -> Unit
is ConvoDialog.ConvoUnpin -> Unit
is ConvoDialog.ConvoArchive -> Unit
is ConvoDialog.ConvoUnarchive -> Unit
}
}
fun onErrorButtonClicked() {
when (baseError.value) {
null -> Unit
is BaseError.ConnectionError,
is BaseError.InternalError,
is BaseError.SimpleError,
is BaseError.UnknownError -> onRefresh()
else -> Unit
}
}
fun onPaginationConditionsMet() {
_currentOffset.update { convos.value.size }
loadConvos()
}
fun onRefresh() {
onErrorConsumed()
loadConvos(offset = 0)
}
fun onConvoItemClick(convo: UiConvo) {
collapseConvos()
_navigation.setValue { ConvoNavigation.MessagesHistory(peerId = convo.id) }
}
fun onConvoItemLongClick(convo: UiConvo) {
expandedConvoId.setValue {
if (convo.isExpanded) 0
else convo.id
}
syncUiConvos()
}
fun onOptionClicked(
convo: UiConvo,
option: ConvoOption
) {
when (option) {
ConvoOption.Delete -> {
_dialog.setValue { ConvoDialog.ConvoDelete(convo.id) }
}
ConvoOption.MarkAsRead -> {
convo.lastMessageId?.let { lastMessageId ->
readConvo(
peerId = convo.id,
startMessageId = lastMessageId
)
collapseConvos()
}
}
ConvoOption.Pin -> {
_dialog.setValue { ConvoDialog.ConvoPin(convo.id) }
}
ConvoOption.Unpin -> {
_dialog.setValue { ConvoDialog.ConvoUnpin(convo.id) }
}
ConvoOption.Archive -> {
_dialog.setValue { ConvoDialog.ConvoArchive(convo.id) }
}
ConvoOption.Unarchive -> {
_dialog.setValue { ConvoDialog.ConvoUnarchive(convo.id) }
}
}
}
fun onErrorConsumed() {
_baseError.setValue { null }
}
fun setScrollIndex(index: Int) {
_screenState.setValue { old -> old.copy(scrollIndex = index) }
}
fun setScrollOffset(offset: Int) {
_screenState.setValue { old -> old.copy(scrollOffset = offset) }
}
fun onCreateChatButtonClicked() {
_navigation.setValue { ConvoNavigation.CreateChat }
}
private fun collapseConvos() {
expandedConvoId.setValue { 0 }
syncUiConvos()
}
private fun loadConvos(
offset: Int = currentOffset.value
) {
convoUseCase.getConvos(
count = LOAD_COUNT,
offset = offset,
filter = filter
).listenValue(viewModelScope) { state ->
state.processState(
error = { error ->
val newBaseError = VkUtils.parseError(error)
_baseError.update { newBaseError }
},
success = { response ->
val convos = response
val fullConvos = if (offset == 0) {
convos
} else {
this.convos.value.plus(convos)
}
val itemsCountSufficient = response.size == LOAD_COUNT
val paginationExhausted = !itemsCountSufficient &&
this.convos.value.isNotEmpty()
_screenState.updateValue {
copy(isPaginationExhausted = paginationExhausted)
}
val imagesToPreload =
response.mapNotNull { it.extractAvatar().extractUrl() }
imagesToPreload.forEach { url ->
imageLoader.enqueue(
ImageRequest.Builder(applicationContext)
.data(url)
.build()
)
}
convoUseCase.storeConvos(response)
_convos.emit(fullConvos)
syncUiConvos()
_canPaginate.setValue { itemsCountSufficient }
}
)
_screenState.setValue { old ->
old.copy(
isLoading = offset == 0 && state.isLoading(),
isPaginating = offset > 0 && state.isLoading()
)
}
}
}
private fun deleteConvo(peerId: Long) {
convoUseCase.delete(peerId).listenValue(viewModelScope) { state ->
state.processState(
error = {},
success = {
val newConvos = convos.value.toMutableList()
val convoIndex =
newConvos.indexOfFirstOrNull { it.id == peerId }
?: return@processState
newConvos.removeAt(convoIndex)
_convos.update { newConvos.sorted() }
syncUiConvos()
}
)
_screenState.emit(screenState.value.copy(isLoading = state.isLoading()))
}
}
private fun pinConvo(peerId: Long, pin: Boolean) {
convoUseCase.changePinState(peerId, pin)
.listenValue(viewModelScope) { state ->
state.processState(
error = {},
success = {
handleChatMajorChanged(
LongPollParsedEvent.ChatMajorChanged(
peerId = peerId,
majorId = if (pin) {
pinnedConvosCount.value.plus(1) * 16
} else {
0
}
)
)
}
)
_screenState.setValue { old -> old.copy(isLoading = state.isLoading()) }
}
}
private fun archiveConvo(peerId: Long, archive: Boolean) {
convoUseCase.changeArchivedState(peerId, archive)
.listenValue(viewModelScope) { state ->
state.processState(
error = {},
success = {
convos.value.find { it.id == peerId }?.let { convo ->
handleChatArchived(
LongPollParsedEvent.ChatArchived(
convo = convo,
archived = archive
)
)
}
}
)
}
}
// TODO: 03-Apr-25, Danil Nikolaev: handle business messages
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
val message = event.message
val newConvos = convos.value.toMutableList()
val convoIndex =
newConvos.indexOfFirstOrNull { it.id == message.peerId }
if (convoIndex == null) {
if (event.inArchive != (filter == ConvosFilter.ARCHIVE)) return
loadConvosByIdUseCase(
peerIds = listOf(message.peerId),
extended = true,
fields = VkConstants.ALL_FIELDS
).listenValue(viewModelScope) { state ->
state.processState(
error = {},
success = { response ->
val convo = (response.firstOrNull() ?: return@listenValue)
.copy(lastMessage = message)
newConvos.add(pinnedConvosCount.value, convo)
_convos.update { newConvos.sorted() }
syncUiConvos()
}
)
}
} else {
val convo = newConvos[convoIndex]
var newConvo = convo.copy(
lastMessage = message,
lastMessageId = message.id,
lastCmId = message.cmId,
unreadCount = if (message.isOut) convo.unreadCount
else convo.unreadCount + 1
)
interactionsTimers[convo.id]?.let { job ->
if (job.interactionType == InteractionType.Typing
&& message.fromId in convo.interactionIds
) {
val newInteractionIds = newConvo.interactionIds.filter { id ->
id != message.fromId
}
newConvo = newConvo.copy(
interactionType = if (newInteractionIds.isEmpty()) -1 else {
newConvo.interactionType
},
interactionIds = newInteractionIds
)
}
}
if (convo.isPinned()) {
newConvos[convoIndex] = newConvo
} else {
newConvos.removeAt(convoIndex)
val toPosition = pinnedConvosCount.value
newConvos.add(toPosition, newConvo)
}
_convos.update { newConvos.sorted() }
syncUiConvos()
}
}
private fun handleEditedMessage(event: LongPollParsedEvent.MessageEdited) {
val message = event.message
val newConvos = convos.value.toMutableList()
val convoIndex = newConvos.indexOfFirstOrNull { it.id == message.peerId }
if (convoIndex == null) { // диалога нет в списке
// pizdets
} else {
val convo = newConvos[convoIndex]
newConvos[convoIndex] = convo.copy(
lastMessage = message,
lastMessageId = message.id,
lastCmId = message.cmId
)
_convos.update { newConvos }
syncUiConvos()
}
}
private fun handleReadIncomingMessage(event: LongPollParsedEvent.IncomingMessageRead) {
val newConvos = convos.value.toMutableList()
val convoIndex =
newConvos.indexOfFirstOrNull { it.id == event.peerId }
if (convoIndex == null) { // диалога нет в списке
// pizdets
} else {
newConvos[convoIndex] =
newConvos[convoIndex].copy(
inReadCmId = event.cmId,
unreadCount = event.unreadCount
)
_convos.update { newConvos }
syncUiConvos()
}
}
private fun handleReadOutgoingMessage(event: LongPollParsedEvent.OutgoingMessageRead) {
val newConvos = convos.value.toMutableList()
val convoIndex =
newConvos.indexOfFirstOrNull { it.id == event.peerId }
if (convoIndex == null) { // диалога нет в списке
// pizdets
} else {
newConvos[convoIndex] =
newConvos[convoIndex].copy(
outReadCmId = event.cmId,
unreadCount = event.unreadCount
)
_convos.update { newConvos }
syncUiConvos()
}
}
private fun handleInteraction(event: LongPollParsedEvent.Interaction) {
val interactionType = event.interactionType
val peerId = event.peerId
val userIds = event.userIds
val newConvos = convos.value.toMutableList()
val convoAndIndex =
newConvos.findWithIndex { it.id == peerId }
if (convoAndIndex != null) {
newConvos[convoAndIndex.first] =
convoAndIndex.second.copy(
interactionType = interactionType.value,
interactionIds = userIds
)
_convos.update { newConvos }
syncUiConvos()
interactionsTimers[peerId]?.let { interactionJob ->
if (interactionJob.interactionType == interactionType) {
interactionJob.timerJob.cancel(NewInteractionException())
}
}
var timeoutAction: (() -> Unit)? = null
val timerJob = createTimerFlow(
time = 6,
onTimeoutAction = { timeoutAction?.invoke() }
).launchIn(viewModelScope)
val newInteractionJob = InteractionJob(
interactionType = interactionType,
timerJob = timerJob
)
interactionsTimers[peerId] = newInteractionJob
timeoutAction = {
stopInteraction(peerId, newInteractionJob)
}
}
}
private fun stopInteraction(peerId: Long, interactionJob: InteractionJob) {
interactionsTimers[peerId] ?: return
val newConvos = convos.value.toMutableList()
val convoAndIndex =
newConvos.findWithIndex { it.id == peerId } ?: return
newConvos[convoAndIndex.first] =
convoAndIndex.second.copy(
interactionType = -1,
interactionIds = emptyList()
)
_convos.update { newConvos }
syncUiConvos()
interactionJob.timerJob.cancel()
interactionsTimers[peerId] = null
}
private fun handleChatMajorChanged(event: LongPollParsedEvent.ChatMajorChanged) {
val newConvos = convos.value.toMutableList()
val convoIndex =
newConvos.indexOfFirstOrNull { it.id == event.peerId }
if (convoIndex == null) { // диалога нет в списке
// pizdets
} else {
newConvos[convoIndex] =
newConvos[convoIndex].copy(majorId = event.majorId)
_convos.setValue { newConvos.sorted() }
syncUiConvos()
}
}
private fun handleChatMinorChanged(event: LongPollParsedEvent.ChatMinorChanged) {
val newConvos = convos.value.toMutableList()
val convoIndex =
newConvos.indexOfFirstOrNull { it.id == event.peerId }
if (convoIndex == null) { // диалога нет в списке
// pizdets
} else {
newConvos[convoIndex] =
newConvos[convoIndex].copy(minorId = event.minorId)
_convos.setValue { newConvos.sorted() }
syncUiConvos()
}
}
private fun handleChatClearing(event: LongPollParsedEvent.ChatCleared) {
val newConvos = convos.value.toMutableList()
val convoIndex = newConvos.indexOfFirstOrNull { it.id == event.peerId }
if (convoIndex == null) { // диалога нет в списке
// pizdets
} else {
newConvos.removeAt(convoIndex)
_convos.setValue { newConvos.sorted() }
syncUiConvos()
}
}
private fun handleChatArchived(event: LongPollParsedEvent.ChatArchived) {
val convo = event.convo
val newConvos = convos.value.toMutableList()
when (filter) {
ConvosFilter.BUSINESS_NOTIFY -> Unit
ConvosFilter.ARCHIVE -> {
if (event.archived) {
newConvos.add(0, convo)
} else {
val index = newConvos.indexOfFirstOrNull { it.id == convo.id }
if (index == null) return
newConvos.removeAt(index)
}
_convos.update { newConvos }
syncUiConvos()
}
else -> {
if (event.archived) {
val index = newConvos.indexOfFirstOrNull { it.id == convo.id }
if (index == null) return
newConvos.removeAt(index)
} else {
newConvos.add(pinnedConvosCount.value, convo)
}
_convos.update { newConvos.sorted() }
syncUiConvos()
}
}
}
private fun readConvo(peerId: Long, startMessageId: Long) {
messagesUseCase.markAsRead(
peerId = peerId,
startMessageId = startMessageId
).listenValue(viewModelScope) { state ->
state.processState(
error = {},
success = {
val newConvos = convos.value.toMutableList()
val convoIndex =
newConvos.indexOfFirstOrNull { it.id == peerId }
?: return@listenValue
newConvos[convoIndex] =
newConvos[convoIndex].copy(inRead = startMessageId)
_convos.update { newConvos }
syncUiConvos()
}
)
}
}
private fun List<VkConvo>.sorted(): List<VkConvo> {
val newConvos = toMutableList()
val pinnedConvos = newConvos
.filter(VkConvo::isPinned)
.sortedWith { c1, c2 ->
val diff = c2.majorId - c1.majorId
if (diff == 0) {
c2.minorId - c1.minorId
} else {
diff
}
}
newConvos.removeAll(pinnedConvos)
newConvos.sortWith { c1, c2 ->
(c2.lastMessage?.date ?: 0) - (c1.lastMessage?.date ?: 0)
}
newConvos.addAll(0, pinnedConvos)
return newConvos
}
private fun syncUiConvos(): List<UiConvo> {
val convos = convos.value
val newUiConvos = convos.map { convo ->
val options = mutableListOf<ConvoOption>()
convo.lastMessage?.run {
if (!convo.isRead() && !this.isOut) {
options += ConvoOption.MarkAsRead
}
}
val convosSize = this.convos.value.size
val pinnedCount = pinnedConvosCount.value
val canPinOneMoreDialog =
convosSize > 4 && pinnedCount < 5 && !convo.isPinned()
if (convo.isPinned()) {
options += ConvoOption.Unpin
} else if (canPinOneMoreDialog) {
options += ConvoOption.Pin
}
when (filter) {
ConvosFilter.ARCHIVE -> ConvoOption.Unarchive
ConvosFilter.UNREAD,
ConvosFilter.ALL -> ConvoOption.Archive
ConvosFilter.BUSINESS_NOTIFY -> null
}?.let(options::add)
options += ConvoOption.Delete
convo.asPresentation(
resources = resources,
useContactName = useContactNames,
isExpanded = expandedConvoId.value == convo.id,
options = options.toImmutableList()
)
}
_uiConvos.setValue { newUiConvos }
return newUiConvos
}
companion object {
const val LOAD_COUNT = 30
}
}
@@ -0,0 +1,37 @@
package dev.meloda.fast.convos.di
import dev.meloda.fast.convos.ConvosViewModel
import dev.meloda.fast.domain.ConvoUseCase
import dev.meloda.fast.domain.ConvoUseCaseImpl
import dev.meloda.fast.model.ConvosFilter
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModel
import org.koin.core.qualifier.named
import org.koin.core.scope.Scope
import org.koin.dsl.bind
import org.koin.dsl.module
val convosModule = module {
viewModel(named(ConvosFilter.ALL)) {
createConvosViewModel(ConvosFilter.ALL)
}
viewModel(named(ConvosFilter.ARCHIVE)) {
createConvosViewModel(ConvosFilter.ARCHIVE)
}
singleOf(::ConvoUseCaseImpl) bind ConvoUseCase::class
}
private fun Scope.createConvosViewModel(filter: ConvosFilter): ConvosViewModel {
return ConvosViewModel(
filter = filter,
updatesParser = get(),
convoUseCase = get(),
messagesUseCase = get(),
resources = get(),
userSettings = get(),
imageLoader = get(),
applicationContext = get(),
loadConvosByIdUseCase = get()
)
}
@@ -0,0 +1,12 @@
package dev.meloda.fast.convos.model
import androidx.compose.runtime.Immutable
@Immutable
sealed class ConvoDialog {
data class ConvoPin(val convoId: Long) : ConvoDialog()
data class ConvoUnpin(val convoId: Long) : ConvoDialog()
data class ConvoDelete(val convoId: Long) : ConvoDialog()
data class ConvoArchive(val convoId: Long) : ConvoDialog()
data class ConvoUnarchive(val convoId: Long) : ConvoDialog()
}
@@ -0,0 +1,11 @@
package dev.meloda.fast.convos.model
import androidx.compose.runtime.Immutable
@Immutable
sealed class ConvoNavigation {
data class MessagesHistory(val peerId: Long) : ConvoNavigation()
data object CreateChat : ConvoNavigation()
}
@@ -1,10 +1,9 @@
package dev.meloda.fast.conversations.model package dev.meloda.fast.convos.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import dev.meloda.fast.ui.model.api.UiConversation
@Immutable @Immutable
data class ConversationsScreenState( data class ConvosScreenState(
val isLoading: Boolean, val isLoading: Boolean,
val isPaginating: Boolean, val isPaginating: Boolean,
val isPaginationExhausted: Boolean, val isPaginationExhausted: Boolean,
@@ -15,7 +14,7 @@ data class ConversationsScreenState(
) { ) {
companion object { companion object {
val EMPTY: ConversationsScreenState = ConversationsScreenState( val EMPTY: ConvosScreenState = ConvosScreenState(
isLoading = true, isLoading = true,
isPaginating = false, isPaginating = false,
isPaginationExhausted = false, isPaginationExhausted = false,
@@ -1,4 +1,4 @@
package dev.meloda.fast.conversations.model package dev.meloda.fast.convos.model
import dev.meloda.fast.model.InteractionType import dev.meloda.fast.model.InteractionType
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -1,4 +1,4 @@
package dev.meloda.fast.conversations.model package dev.meloda.fast.convos.model
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@@ -1,13 +1,13 @@
package dev.meloda.fast.conversations.navigation package dev.meloda.fast.convos.navigation
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.navigation.NavGraphBuilder import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.navigation import androidx.navigation.navigation
import dev.meloda.fast.conversations.ConversationsViewModel import dev.meloda.fast.convos.ConvosViewModel
import dev.meloda.fast.conversations.presentation.ConversationsRoute import dev.meloda.fast.convos.presentation.ConvosRoute
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.ConversationsFilter import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.ui.extensions.getOrThrow import dev.meloda.fast.ui.extensions.getOrThrow
import dev.meloda.fast.ui.theme.LocalNavController import dev.meloda.fast.ui.theme.LocalNavController
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@@ -15,32 +15,32 @@ import org.koin.androidx.viewmodel.ext.android.getViewModel
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
@Serializable @Serializable
object ConversationsGraph object ConvoGraph
@Serializable @Serializable
object Conversations object Convos
@Serializable @Serializable
object Archive object Archive
fun NavGraphBuilder.conversationsGraph( fun NavGraphBuilder.convosGraph(
activity: AppCompatActivity, activity: AppCompatActivity,
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onNavigateToMessagesHistory: (id: Long) -> Unit, onNavigateToMessagesHistory: (id: Long) -> Unit,
onNavigateToCreateChat: () -> Unit, onNavigateToCreateChat: () -> Unit,
onScrolledToTop: () -> Unit onScrolledToTop: () -> Unit
) { ) {
navigation<ConversationsGraph>( navigation<ConvoGraph>(
startDestination = Conversations startDestination = Convos
) { ) {
val conversationsViewModel: ConversationsViewModel = with(activity) { val convosViewModel: ConvosViewModel = with(activity) {
getViewModel(qualifier = named(ConversationsFilter.ALL)) getViewModel(qualifier = named(ConvosFilter.ALL))
} }
composable<Conversations> { composable<Convos> {
val navController = LocalNavController.getOrThrow() val navController = LocalNavController.getOrThrow()
ConversationsRoute( ConvosRoute(
viewModel = conversationsViewModel, viewModel = convosViewModel,
onError = onError, onError = onError,
onNavigateToMessagesHistory = onNavigateToMessagesHistory, onNavigateToMessagesHistory = onNavigateToMessagesHistory,
onNavigateToCreateChat = onNavigateToCreateChat, onNavigateToCreateChat = onNavigateToCreateChat,
@@ -51,10 +51,10 @@ fun NavGraphBuilder.conversationsGraph(
composable<Archive> { composable<Archive> {
val navController = LocalNavController.getOrThrow() val navController = LocalNavController.getOrThrow()
ConversationsRoute( ConvosRoute(
viewModel = with(activity) { viewModel = with(activity) {
getViewModel<ConversationsViewModel>( getViewModel<ConvosViewModel>(
qualifier = named(ConversationsFilter.ARCHIVE) qualifier = named(ConvosFilter.ARCHIVE)
) )
}, },
onBack = navController::navigateUp, onBack = navController::navigateUp,
@@ -1,70 +1,70 @@
package dev.meloda.fast.conversations.presentation package dev.meloda.fast.convos.presentation
import android.os.Bundle import android.os.Bundle
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import dev.meloda.fast.conversations.model.ConversationDialog import dev.meloda.fast.convos.model.ConvoDialog
import dev.meloda.fast.conversations.model.ConversationsScreenState import dev.meloda.fast.convos.model.ConvosScreenState
import dev.meloda.fast.ui.components.MaterialDialog import dev.meloda.fast.ui.components.MaterialDialog
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
@Composable @Composable
fun HandleDialogs( fun HandleDialogs(
screenState: ConversationsScreenState, screenState: ConvosScreenState,
dialog: ConversationDialog?, dialog: ConvoDialog?,
onConfirmed: (ConversationDialog, Bundle) -> Unit = { _, _ -> }, onConfirmed: (ConvoDialog, Bundle) -> Unit = { _, _ -> },
onDismissed: (ConversationDialog) -> Unit = {}, onDismissed: (ConvoDialog) -> Unit = {},
onItemPicked: (ConversationDialog, Bundle) -> Unit = { _, _ -> } onItemPicked: (ConvoDialog, Bundle) -> Unit = { _, _ -> }
) { ) {
when (dialog) { when (dialog) {
null -> Unit null -> Unit
is ConversationDialog.ConversationArchive -> { is ConvoDialog.ConvoArchive -> {
MaterialDialog( MaterialDialog(
onDismissRequest = { onDismissed(dialog) }, onDismissRequest = { onDismissed(dialog) },
title = stringResource(id = R.string.confirm_archive_conversation), title = stringResource(id = R.string.confirm_archive_convo),
confirmAction = { onConfirmed(dialog, bundleOf()) }, confirmAction = { onConfirmed(dialog, bundleOf()) },
confirmText = stringResource(id = R.string.action_archive), confirmText = stringResource(id = R.string.action_archive),
cancelText = stringResource(id = R.string.cancel) cancelText = stringResource(id = R.string.cancel)
) )
} }
is ConversationDialog.ConversationUnarchive -> { is ConvoDialog.ConvoUnarchive -> {
MaterialDialog( MaterialDialog(
onDismissRequest = { onDismissed(dialog) }, onDismissRequest = { onDismissed(dialog) },
title = stringResource(id = R.string.confirm_unarchive_conversation), title = stringResource(id = R.string.confirm_unarchive_convo),
confirmAction = { onConfirmed(dialog, bundleOf()) }, confirmAction = { onConfirmed(dialog, bundleOf()) },
confirmText = stringResource(id = R.string.action_unarchive), confirmText = stringResource(id = R.string.action_unarchive),
cancelText = stringResource(id = R.string.cancel) cancelText = stringResource(id = R.string.cancel)
) )
} }
is ConversationDialog.ConversationDelete -> { is ConvoDialog.ConvoDelete -> {
MaterialDialog( MaterialDialog(
onDismissRequest = { onDismissed(dialog) }, onDismissRequest = { onDismissed(dialog) },
title = stringResource(id = R.string.confirm_delete_conversation), title = stringResource(id = R.string.confirm_delete_convo),
confirmAction = { onConfirmed(dialog, bundleOf()) }, confirmAction = { onConfirmed(dialog, bundleOf()) },
confirmText = stringResource(id = R.string.action_delete), confirmText = stringResource(id = R.string.action_delete),
cancelText = stringResource(id = R.string.cancel) cancelText = stringResource(id = R.string.cancel)
) )
} }
is ConversationDialog.ConversationPin -> { is ConvoDialog.ConvoPin -> {
MaterialDialog( MaterialDialog(
onDismissRequest = { onDismissed(dialog) }, onDismissRequest = { onDismissed(dialog) },
title = stringResource(id = R.string.confirm_pin_conversation), title = stringResource(id = R.string.confirm_pin_convo),
confirmAction = { onConfirmed(dialog, bundleOf()) }, confirmAction = { onConfirmed(dialog, bundleOf()) },
confirmText = stringResource(id = R.string.action_pin), confirmText = stringResource(id = R.string.action_pin),
cancelText = stringResource(id = R.string.cancel) cancelText = stringResource(id = R.string.cancel)
) )
} }
is ConversationDialog.ConversationUnpin -> { is ConvoDialog.ConvoUnpin -> {
MaterialDialog( MaterialDialog(
onDismissRequest = { onDismissed(dialog) }, onDismissRequest = { onDismissed(dialog) },
title = stringResource(id = R.string.confirm_unpin_conversation), title = stringResource(id = R.string.confirm_unpin_convo),
confirmAction = { onConfirmed(dialog, bundleOf()) }, confirmAction = { onConfirmed(dialog, bundleOf()) },
confirmText = stringResource(id = R.string.action_unpin), confirmText = stringResource(id = R.string.action_unpin),
cancelText = stringResource(id = R.string.cancel) cancelText = stringResource(id = R.string.cancel)
@@ -1,4 +1,4 @@
package dev.meloda.fast.conversations.presentation package dev.meloda.fast.convos.presentation
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
@@ -48,8 +48,8 @@ import coil.compose.AsyncImage
import dev.meloda.fast.ui.basic.ContentAlpha import dev.meloda.fast.ui.basic.ContentAlpha
import dev.meloda.fast.ui.basic.LocalContentAlpha import dev.meloda.fast.ui.basic.LocalContentAlpha
import dev.meloda.fast.ui.components.DotsFlashing import dev.meloda.fast.ui.components.DotsFlashing
import dev.meloda.fast.ui.model.api.ConversationOption import dev.meloda.fast.ui.model.vk.ConvoOption
import dev.meloda.fast.ui.model.api.UiConversation import dev.meloda.fast.ui.model.vk.UiConvo
import dev.meloda.fast.ui.util.getImage import dev.meloda.fast.ui.util.getImage
import dev.meloda.fast.ui.util.getResourcePainter import dev.meloda.fast.ui.util.getResourcePainter
import dev.meloda.fast.ui.util.getString import dev.meloda.fast.ui.util.getString
@@ -59,19 +59,19 @@ val BirthdayColor = Color(0xffb00b69)
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun ConversationItem( fun ConvoItem(
onItemClick: (UiConversation) -> Unit, onItemClick: (UiConvo) -> Unit,
onItemLongClick: (conversation: UiConversation) -> Unit, onItemLongClick: (convo: UiConvo) -> Unit,
onOptionClicked: (UiConversation, ConversationOption) -> Unit, onOptionClicked: (UiConvo, ConvoOption) -> Unit,
maxLines: Int, maxLines: Int,
isUserAccount: Boolean, isUserAccount: Boolean,
conversation: UiConversation, convo: UiConvo,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val hapticFeedback = LocalHapticFeedback.current val hapticFeedback = LocalHapticFeedback.current
val bottomStartCornerRadius by animateDpAsState( val bottomStartCornerRadius by animateDpAsState(
targetValue = if (conversation.isExpanded) 10.dp else 34.dp, targetValue = if (convo.isExpanded) 10.dp else 34.dp,
label = "bottomStartCornerRadius" label = "bottomStartCornerRadius"
) )
@@ -79,15 +79,15 @@ fun ConversationItem(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.combinedClickable( .combinedClickable(
onClick = { onItemClick(conversation) }, onClick = { onItemClick(convo) },
onLongClick = { onLongClick = {
onItemLongClick(conversation) onItemLongClick(convo)
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
} }
) )
) { ) {
val showBackground by remember(conversation) { val showBackground by remember(convo) {
derivedStateOf { conversation.isUnread || conversation.isExpanded } derivedStateOf { convo.isUnread || convo.isExpanded }
} }
AnimatedVisibility( AnimatedVisibility(
@@ -133,7 +133,7 @@ fun ConversationItem(
) )
} }
} else { } else {
val avatarImage = conversation.avatar?.getImage() val avatarImage = convo.avatar?.getImage()
if (avatarImage is Painter) { if (avatarImage is Painter) {
Icon( Icon(
modifier = Modifier modifier = Modifier
@@ -155,7 +155,7 @@ fun ConversationItem(
} }
} }
if (conversation.isPinned) { if (convo.isPinned) {
Box( Box(
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
@@ -173,13 +173,13 @@ fun ConversationItem(
} }
} }
if (conversation.isOnline) { if (convo.isOnline) {
Box( Box(
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.size(18.dp) .size(18.dp)
.background( .background(
if (conversation.isUnread) { if (convo.isUnread) {
MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp) MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)
} else { } else {
MaterialTheme.colorScheme.background MaterialTheme.colorScheme.background
@@ -197,13 +197,13 @@ fun ConversationItem(
} }
} }
if (conversation.isBirthday) { if (convo.isBirthday) {
Box( Box(
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.size(16.dp) .size(16.dp)
.background( .background(
if (conversation.isUnread) { if (convo.isUnread) {
MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp) MaterialTheme.colorScheme.surfaceColorAtElevation(5.dp)
} else { } else {
MaterialTheme.colorScheme.background MaterialTheme.colorScheme.background
@@ -237,16 +237,16 @@ fun ConversationItem(
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Text( Text(
text = conversation.title, text = convo.title,
minLines = 1, minLines = 1,
maxLines = maxLines, maxLines = maxLines,
style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp), style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp),
) )
Row { Row {
if (conversation.interactionText != null) { if (convo.interactionText != null) {
Text( Text(
text = conversation.interactionText.orEmpty(), text = convo.interactionText.orEmpty(),
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Spacer(modifier = Modifier.width(4.dp)) Spacer(modifier = Modifier.width(4.dp))
@@ -258,7 +258,7 @@ fun ConversationItem(
dotColor = MaterialTheme.colorScheme.primary dotColor = MaterialTheme.colorScheme.primary
) )
} else { } else {
conversation.attachmentImage?.getResourcePainter()?.let { painter -> convo.attachmentImage?.getResourcePainter()?.let { painter ->
Column { Column {
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Icon( Icon(
@@ -277,9 +277,9 @@ fun ConversationItem(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
text = kotlin.run { text = kotlin.run {
val builder = val builder =
AnnotatedString.Builder(conversation.message.text) AnnotatedString.Builder(convo.message.text)
conversation.message.spanStyles.map { spanStyleRange -> convo.message.spanStyles.map { spanStyleRange ->
val updatedSpanStyle = val updatedSpanStyle =
if (spanStyleRange.item.color == Color.Red) { if (spanStyleRange.item.color == Color.Red) {
spanStyleRange.item.copy(color = MaterialTheme.colorScheme.primary) spanStyleRange.item.copy(color = MaterialTheme.colorScheme.primary)
@@ -294,7 +294,7 @@ fun ConversationItem(
) )
} }
conversation.message.paragraphStyles.forEach { style -> convo.message.paragraphStyles.forEach { style ->
builder.addStyle( builder.addStyle(
style = style.item, style = style.item,
start = style.start, start = style.start,
@@ -318,12 +318,12 @@ fun ConversationItem(
Column { Column {
LocalContentAlpha(alpha = ContentAlpha.medium) { LocalContentAlpha(alpha = ContentAlpha.medium) {
Text( Text(
text = conversation.date, text = convo.date,
style = MaterialTheme.typography.bodySmall style = MaterialTheme.typography.bodySmall
) )
} }
conversation.unreadCount?.let { count -> convo.unreadCount?.let { count ->
Spacer(modifier = Modifier.height(6.dp)) Spacer(modifier = Modifier.height(6.dp))
Box( Box(
modifier = Modifier modifier = Modifier
@@ -351,7 +351,7 @@ fun ConversationItem(
Spacer(modifier = Modifier.width(24.dp)) Spacer(modifier = Modifier.width(24.dp))
} }
AnimatedVisibility(conversation.isExpanded) { AnimatedVisibility(convo.isExpanded) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -367,9 +367,9 @@ fun ConversationItem(
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 10.dp) .padding(horizontal = 10.dp)
) { ) {
items(conversation.options.toList()) { option -> items(convo.options.toList()) { option ->
ElevatedAssistChip( ElevatedAssistChip(
onClick = { onOptionClicked(conversation, option) }, onClick = { onOptionClicked(convo, option) },
leadingIcon = { leadingIcon = {
option.icon.getResourcePainter()?.let { painter -> option.icon.getResourcePainter()?.let { painter ->
Icon( Icon(
@@ -390,7 +390,7 @@ fun ConversationItem(
} }
val bottomSpacerHeight by animateDpAsState( val bottomSpacerHeight by animateDpAsState(
targetValue = if (conversation.isExpanded) 4.dp else 8.dp, targetValue = if (convo.isExpanded) 4.dp else 8.dp,
label = "bottomSpacerHeight" label = "bottomSpacerHeight"
) )
@@ -1,4 +1,4 @@
package dev.meloda.fast.conversations.presentation package dev.meloda.fast.convos.presentation
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -21,11 +21,11 @@ import androidx.compose.ui.Alignment
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.unit.dp import androidx.compose.ui.unit.dp
import dev.meloda.fast.conversations.model.ConversationsScreenState import dev.meloda.fast.convos.model.ConvosScreenState
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.model.api.ConversationOption import dev.meloda.fast.ui.model.vk.ConvoOption
import dev.meloda.fast.ui.model.api.UiConversation import dev.meloda.fast.ui.model.vk.UiConvo
import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalBottomPadding
import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.theme.LocalThemeConfig
import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.ImmutableList
@@ -33,15 +33,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
fun ConversationsList( fun ConvosList(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
conversations: ImmutableList<UiConversation>, convos: ImmutableList<UiConvo>,
onConversationsClick: (UiConversation) -> Unit, onConvosClick: (UiConvo) -> Unit,
onConversationsLongClick: (UiConversation) -> Unit, onConvosLongClick: (UiConvo) -> Unit,
screenState: ConversationsScreenState, screenState: ConvosScreenState,
state: LazyListState, state: LazyListState,
maxLines: Int, maxLines: Int,
onOptionClicked: (UiConversation, ConversationOption) -> Unit, onOptionClicked: (UiConvo, ConvoOption) -> Unit,
padding: PaddingValues padding: PaddingValues
) { ) {
val theme = LocalThemeConfig.current val theme = LocalThemeConfig.current
@@ -56,22 +56,22 @@ fun ConversationsList(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
} }
items( items(
items = conversations.values, items = convos.values,
key = UiConversation::id, key = UiConvo::id,
) { conversation -> ) { convo ->
val isUserAccount by remember(conversation) { val isUserAccount by remember(convo) {
derivedStateOf { derivedStateOf {
conversation.id == UserConfig.userId convo.id == UserConfig.userId
} }
} }
ConversationItem( ConvoItem(
onItemClick = onConversationsClick, onItemClick = onConvosClick,
onItemLongClick = onConversationsLongClick, onItemLongClick = onConvosLongClick,
onOptionClicked = onOptionClicked, onOptionClicked = onOptionClicked,
maxLines = maxLines, maxLines = maxLines,
isUserAccount = isUserAccount, isUserAccount = isUserAccount,
conversation = conversation, convo = convo,
modifier = modifier =
if (theme.enableAnimations) Modifier.animateItem( if (theme.enableAnimations) Modifier.animateItem(
fadeInSpec = null, fadeInSpec = null,
@@ -1,27 +1,27 @@
package dev.meloda.fast.conversations.presentation package dev.meloda.fast.convos.presentation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.meloda.fast.conversations.ConversationsViewModel import dev.meloda.fast.convos.ConvosViewModel
import dev.meloda.fast.conversations.model.ConversationNavigation import dev.meloda.fast.convos.model.ConvoNavigation
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
@Composable @Composable
fun ConversationsRoute( fun ConvosRoute(
viewModel: ConversationsViewModel, viewModel: ConvosViewModel,
onBack: (() -> Unit)? = null, onBack: (() -> Unit)? = null,
onError: (BaseError) -> Unit, onError: (BaseError) -> Unit,
onNavigateToMessagesHistory: (conversationId: Long) -> Unit, onNavigateToMessagesHistory: (convoId: Long) -> Unit,
onNavigateToCreateChat: (() -> Unit)? = null, onNavigateToCreateChat: (() -> Unit)? = null,
onNavigateToArchive: (() -> Unit)? = null, onNavigateToArchive: (() -> Unit)? = null,
onScrolledToTop: () -> Unit, onScrolledToTop: () -> Unit,
) { ) {
val screenState by viewModel.screenState.collectAsStateWithLifecycle() val screenState by viewModel.screenState.collectAsStateWithLifecycle()
val navigationEvent by viewModel.navigation.collectAsStateWithLifecycle() val navigationEvent by viewModel.navigation.collectAsStateWithLifecycle()
val conversations by viewModel.uiConversations.collectAsStateWithLifecycle() val convos by viewModel.uiConvos.collectAsStateWithLifecycle()
val dialog by viewModel.dialog.collectAsStateWithLifecycle() val dialog by viewModel.dialog.collectAsStateWithLifecycle()
val baseError by viewModel.baseError.collectAsStateWithLifecycle() val baseError by viewModel.baseError.collectAsStateWithLifecycle()
val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle()
@@ -30,12 +30,12 @@ fun ConversationsRoute(
val shouldBeConsumed: Boolean = when (val navigation = navigationEvent) { val shouldBeConsumed: Boolean = when (val navigation = navigationEvent) {
null -> false null -> false
is ConversationNavigation.CreateChat -> { is ConvoNavigation.CreateChat -> {
onNavigateToCreateChat?.invoke() onNavigateToCreateChat?.invoke()
true true
} }
is ConversationNavigation.MessagesHistory -> { is ConvoNavigation.MessagesHistory -> {
onNavigateToMessagesHistory(navigation.peerId) onNavigateToMessagesHistory(navigation.peerId)
true true
} }
@@ -44,14 +44,14 @@ fun ConversationsRoute(
if (shouldBeConsumed) viewModel.onNavigationConsumed() if (shouldBeConsumed) viewModel.onNavigationConsumed()
} }
ConversationsScreen( ConvosScreen(
onBack = { onBack?.invoke() }, onBack = { onBack?.invoke() },
screenState = screenState, screenState = screenState,
conversations = conversations.toImmutableList(), convos = convos.toImmutableList(),
baseError = baseError, baseError = baseError,
canPaginate = canPaginate, canPaginate = canPaginate,
onConversationItemClicked = viewModel::onConversationItemClick, onConvoItemClicked = viewModel::onConvoItemClick,
onConversationItemLongClicked = viewModel::onConversationItemLongClick, onConvoItemLongClicked = viewModel::onConvoItemLongClick,
onOptionClicked = viewModel::onOptionClicked, onOptionClicked = viewModel::onOptionClicked,
onPaginationConditionsMet = viewModel::onPaginationConditionsMet, onPaginationConditionsMet = viewModel::onPaginationConditionsMet,
onRefreshDropdownItemClicked = viewModel::onRefresh, onRefreshDropdownItemClicked = viewModel::onRefresh,
@@ -1,4 +1,4 @@
package dev.meloda.fast.conversations.presentation package dev.meloda.fast.convos.presentation
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
@@ -57,16 +57,16 @@ import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.conversations.model.ConversationsScreenState import dev.meloda.fast.convos.model.ConvosScreenState
import dev.meloda.fast.conversations.navigation.ConversationsGraph import dev.meloda.fast.convos.navigation.ConvoGraph
import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.components.FullScreenContainedLoader import dev.meloda.fast.ui.components.FullScreenContainedLoader
import dev.meloda.fast.ui.components.NoItemsView import dev.meloda.fast.ui.components.NoItemsView
import dev.meloda.fast.ui.components.VkErrorView import dev.meloda.fast.ui.components.VkErrorView
import dev.meloda.fast.ui.model.api.ConversationOption import dev.meloda.fast.ui.model.vk.ConvoOption
import dev.meloda.fast.ui.model.api.UiConversation import dev.meloda.fast.ui.model.vk.UiConvo
import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalBottomPadding
import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalHazeState
import dev.meloda.fast.ui.theme.LocalReselectedTab import dev.meloda.fast.ui.theme.LocalReselectedTab
@@ -82,15 +82,15 @@ import kotlinx.coroutines.flow.debounce
ExperimentalHazeMaterialsApi::class, ExperimentalMaterial3ExpressiveApi::class, ExperimentalHazeMaterialsApi::class, ExperimentalMaterial3ExpressiveApi::class,
) )
@Composable @Composable
fun ConversationsScreen( fun ConvosScreen(
screenState: ConversationsScreenState = ConversationsScreenState.EMPTY, screenState: ConvosScreenState = ConvosScreenState.EMPTY,
conversations: ImmutableList<UiConversation> = emptyImmutableList(), convos: ImmutableList<UiConvo> = emptyImmutableList(),
baseError: BaseError? = null, baseError: BaseError? = null,
canPaginate: Boolean = false, canPaginate: Boolean = false,
onBack: () -> Unit = {}, onBack: () -> Unit = {},
onConversationItemClicked: (conversation: UiConversation) -> Unit = {}, onConvoItemClicked: (convo: UiConvo) -> Unit = {},
onConversationItemLongClicked: (conversation: UiConversation) -> Unit = {}, onConvoItemLongClicked: (convo: UiConvo) -> Unit = {},
onOptionClicked: (UiConversation, ConversationOption) -> Unit = { _, _ -> }, onOptionClicked: (UiConvo, ConvoOption) -> Unit = { _, _ -> },
onPaginationConditionsMet: () -> Unit = {}, onPaginationConditionsMet: () -> Unit = {},
onRefreshDropdownItemClicked: () -> Unit = {}, onRefreshDropdownItemClicked: () -> Unit = {},
onRefresh: () -> Unit = {}, onRefresh: () -> Unit = {},
@@ -109,7 +109,7 @@ fun ConversationsScreen(
initialFirstVisibleItemScrollOffset = screenState.scrollOffset initialFirstVisibleItemScrollOffset = screenState.scrollOffset
) )
val currentTabReselected = LocalReselectedTab.current[ConversationsGraph] == true val currentTabReselected = LocalReselectedTab.current[ConvoGraph] == true
LaunchedEffect(currentTabReselected) { LaunchedEffect(currentTabReselected) {
if (currentTabReselected) { if (currentTabReselected) {
if (screenState.isArchive) { if (screenState.isArchive) {
@@ -182,7 +182,7 @@ fun ConversationsScreen(
id = when { id = when {
screenState.isLoading -> R.string.title_loading screenState.isLoading -> R.string.title_loading
screenState.isArchive -> R.string.title_archive screenState.isArchive -> R.string.title_archive
else -> R.string.title_conversations else -> R.string.title_convos
} }
), ),
maxLines = 1, maxLines = 1,
@@ -268,7 +268,7 @@ fun ConversationsScreen(
) )
val showHorizontalProgressBar by remember(screenState) { val showHorizontalProgressBar by remember(screenState) {
derivedStateOf { screenState.isLoading && conversations.isNotEmpty() } derivedStateOf { screenState.isLoading && convos.isNotEmpty() }
} }
AnimatedVisibility(showHorizontalProgressBar) { AnimatedVisibility(showHorizontalProgressBar) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
@@ -310,7 +310,7 @@ fun ConversationsScreen(
) )
} }
screenState.isLoading && conversations.isEmpty() -> FullScreenContainedLoader() screenState.isLoading && convos.isEmpty() -> FullScreenContainedLoader()
else -> { else -> {
val pullToRefreshState = rememberPullToRefreshState() val pullToRefreshState = rememberPullToRefreshState()
@@ -334,10 +334,10 @@ fun ConversationsScreen(
) )
} }
) { ) {
ConversationsList( ConvosList(
conversations = conversations, convos = convos,
onConversationsClick = onConversationItemClicked, onConvosClick = onConvoItemClicked,
onConversationsLongClick = onConversationItemLongClicked, onConvosLongClick = onConvoItemLongClicked,
screenState = screenState, screenState = screenState,
state = listState, state = listState,
maxLines = maxLines, maxLines = maxLines,
@@ -350,7 +350,7 @@ fun ConversationsScreen(
padding = padding padding = padding
) )
if (conversations.isEmpty()) { if (convos.isEmpty()) {
NoItemsView( NoItemsView(
buttonText = stringResource(R.string.action_refresh), buttonText = stringResource(R.string.action_refresh),
onButtonClick = onRefresh onButtonClick = onRefresh
@@ -1,4 +1,4 @@
package dev.meloda.fast.conversations package dev.meloda.fast.convos
import android.content.Context import android.content.Context
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -7,7 +7,7 @@ import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.listenValue
import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.extensions.setValue
import dev.meloda.fast.conversations.model.CreateChatScreenState import dev.meloda.fast.convos.model.CreateChatScreenState
import dev.meloda.fast.data.State import dev.meloda.fast.data.State
import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.processState import dev.meloda.fast.data.processState
@@ -19,7 +19,7 @@ import dev.meloda.fast.domain.util.asPresentation
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.model.api.domain.VkUser
import dev.meloda.fast.network.VkErrorCode import dev.meloda.fast.network.VkErrorCode
import dev.meloda.fast.ui.model.api.UiFriend import dev.meloda.fast.ui.model.vk.UiFriend
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@@ -1,6 +1,6 @@
package dev.meloda.fast.conversations.di package dev.meloda.fast.convos.di
import dev.meloda.fast.conversations.CreateChatViewModel import dev.meloda.fast.convos.CreateChatViewModel
import org.koin.core.module.dsl.viewModelOf import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module import org.koin.dsl.module
@@ -1,7 +1,7 @@
package dev.meloda.fast.conversations.model package dev.meloda.fast.convos.model
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
import dev.meloda.fast.ui.model.api.UiFriend import dev.meloda.fast.ui.model.vk.UiFriend
@Immutable @Immutable
data class CreateChatScreenState( data class CreateChatScreenState(
@@ -1,12 +1,12 @@
package dev.meloda.fast.conversations.navigation package dev.meloda.fast.convos.navigation
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
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 dev.meloda.fast.conversations.CreateChatViewModel import dev.meloda.fast.convos.CreateChatViewModel
import dev.meloda.fast.conversations.presentation.CreateChatRoute import dev.meloda.fast.convos.presentation.CreateChatRoute
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
@@ -1,4 +1,4 @@
package dev.meloda.fast.conversations.presentation package dev.meloda.fast.convos.presentation
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
@@ -25,7 +25,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.model.api.UiFriend import dev.meloda.fast.ui.model.vk.UiFriend
@Composable @Composable
@@ -1,4 +1,4 @@
package dev.meloda.fast.conversations.presentation package dev.meloda.fast.convos.presentation
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -19,9 +19,9 @@ import androidx.compose.ui.Alignment
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.unit.dp import androidx.compose.ui.unit.dp
import dev.meloda.fast.conversations.model.CreateChatScreenState import dev.meloda.fast.convos.model.CreateChatScreenState
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.model.api.UiFriend import dev.meloda.fast.ui.model.vk.UiFriend
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -1,4 +1,4 @@
package dev.meloda.fast.conversations.presentation package dev.meloda.fast.convos.presentation
import androidx.compose.animation.animateColorAsState import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutLinearInEasing
@@ -58,8 +58,8 @@ import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.conversations.CreateChatViewModel import dev.meloda.fast.convos.CreateChatViewModel
import dev.meloda.fast.conversations.model.CreateChatScreenState import dev.meloda.fast.convos.model.CreateChatScreenState
import dev.meloda.fast.model.BaseError import dev.meloda.fast.model.BaseError
import dev.meloda.fast.ui.R import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.components.FullScreenContainedLoader import dev.meloda.fast.ui.components.FullScreenContainedLoader

Some files were not shown because too many files have changed in this diff Show More