* 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
@@ -138,3 +138,17 @@ fun <T : Any> Bundle.getParcelableCompat(key: String, clazz: KClass<T>): T? {
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
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.VkMessage
import kotlin.math.abs
@@ -16,9 +16,9 @@ class VkGroupsMap(
fun groups(): List<VkGroupDomain> = map.values.toList()
fun conversationGroup(conversation: VkConversation): VkGroupDomain? =
if (!conversation.peerType.isGroup()) null
else map[abs(conversation.id)]
fun convoGroup(convo: VkConvo): VkGroupDomain? =
if (!convo.peerType.isGroup()) null
else map[abs(convo.id)]
fun messageActionGroup(message: VkMessage): VkGroupDomain? =
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.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.VkMessage
import dev.meloda.fast.model.api.domain.VkUser
@@ -13,7 +13,7 @@ object VkMemoryCache {
private val users: HashMap<Long, VkUser> = hashMapOf()
private val groups: HashMap<Long, VkGroupDomain> = 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()
fun appendUsers(users: List<VkUser>) {
@@ -28,9 +28,9 @@ object VkMemoryCache {
messages.forEach { message -> VkMemoryCache.messages[message.id] = message }
}
fun appendConversations(conversations: List<VkConversation>) {
conversations.forEach { conversation ->
VkMemoryCache.conversations[conversation.id] = conversation
fun appendConvos(convos: List<VkConvo>) {
convos.forEach { convo ->
VkMemoryCache.convos[convo.id] = convo
}
}
@@ -50,8 +50,8 @@ object VkMemoryCache {
messages[messageId] = message
}
operator fun set(conversationId: Long, conversation: VkConversation) {
conversations[conversationId] = conversation
operator fun set(convoId: Long, convo: VkConvo) {
convos[convoId] = convo
}
operator fun set(contactId: Long, contact: VkContactDomain) {
@@ -94,16 +94,16 @@ object VkMemoryCache {
return ids.mapNotNull { id -> messages[id] }
}
fun getConversation(id: Long): VkConversation? {
return getConversations(id).firstOrNull()
fun getConvo(id: Long): VkConvo? {
return getConvos(id).firstOrNull()
}
fun getConversations(vararg ids: Long): List<VkConversation> {
return getConversations(ids.toList())
fun getConvos(vararg ids: Long): List<VkConvo> {
return getConvos(ids.toList())
}
fun getConversations(ids: List<Long>): List<VkConversation> {
return ids.mapNotNull { id -> conversations[id] }
fun getConvos(ids: List<Long>): List<VkConvo> {
return ids.mapNotNull { id -> convos[id] }
}
fun getContact(id: Long): VkContactDomain? {
@@ -1,8 +1,7 @@
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.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.VkUser
@@ -16,9 +15,9 @@ class VkUsersMap(
fun users(): List<VkUser> = map.values.toList()
fun conversationUser(conversation: VkConversation): VkUser? =
if (!conversation.peerType.isUser()) null
else map[conversation.id]
fun convoUser(convo: VkConvo): VkUser? =
if (!convo.peerType.isUser()) null
else map[convo.id]
fun messageActionUser(message: VkMessage): VkUser? =
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 dev.meloda.fast.model.ConversationsFilter
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.domain.VkConvo
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?,
offset: Int?,
filter: ConversationsFilter
): ApiResult<List<VkConversation>, RestApiErrorDomain>
filter: ConvosFilter
): ApiResult<List<VkConvo>, RestApiErrorDomain>
suspend fun getConversationsById(
suspend fun getConvosById(
peerIds: List<Long>,
extended: Boolean? = null,
fields: String? = null
): ApiResult<List<VkConversation>, RestApiErrorDomain>
): ApiResult<List<VkConvo>, RestApiErrorDomain>
suspend fun delete(peerId: Long): ApiResult<Long, 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 dev.meloda.fast.common.VkConstants
import dev.meloda.fast.data.VkGroupsMap
import dev.meloda.fast.data.VkMemoryCache
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.MessageDao
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.VkGroupData
import dev.meloda.fast.model.api.data.VkUserData
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.VkMessage
import dev.meloda.fast.model.api.domain.VkUser
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.mapApiDefault
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.launch
import kotlinx.coroutines.withContext
class ConversationsRepositoryImpl(
private val conversationsService: ConversationsService,
class ConvosRepositoryImpl(
private val convosService: ConvosService,
private val messageDao: MessageDao,
private val userDao: UserDao,
private val groupDao: GroupDao,
private val conversationDao: ConversationDao
) : ConversationsRepository {
private val convoDao: ConvoDao
) : ConvosRepository {
override suspend fun storeConversations(conversations: List<VkConversation>) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
override suspend fun storeConvos(convos: List<VkConvo>) {
convoDao.insertAll(convos.map(VkConvo::asEntity))
}
override suspend fun getConversations(
override suspend fun getConvos(
count: Int?,
offset: Int?,
filter: ConversationsFilter
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConversationsGetRequest(
filter: ConvosFilter
): ApiResult<List<VkConvo>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConvosGetRequest(
count = count,
offset = offset,
fields = VkConstants.ALL_FIELDS,
@@ -54,7 +54,7 @@ class ConversationsRepositoryImpl(
startMessageId = null
)
conversationsService.getConversations(requestModel.map).mapApiResult(
convosService.getConvos(requestModel.map).mapApiResult(
successMapper = { apiResponse ->
val response = apiResponse.requireResponse()
@@ -69,7 +69,7 @@ class ConversationsRepositoryImpl(
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
val conversations = response.items.map { item ->
val convos = response.items.map { item ->
val lastMessage = item.lastMessage?.asDomain()?.let { message ->
message.copy(
user = usersMap.messageUser(message),
@@ -84,24 +84,24 @@ class ConversationsRepositoryImpl(
)
).also { VkMemoryCache[message.id] = it }
}
item.conversation.asDomain(lastMessage).let { conversation ->
conversation.copy(
user = usersMap.conversationUser(conversation),
group = groupsMap.conversationGroup(conversation)
).also { VkMemoryCache[conversation.id] = it }
item.convo.asDomain(lastMessage).let { convo ->
convo.copy(
user = usersMap.convoUser(convo),
group = groupsMap.convoGroup(convo)
).also { VkMemoryCache[convo.id] = it }
}
}
val messages = conversations.mapNotNull(VkConversation::lastMessage)
val messages = convos.mapNotNull(VkConvo::lastMessage)
launch(Dispatchers.IO) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
convoDao.insertAll(convos.map(VkConvo::asEntity))
messageDao.insertAll(messages.map(VkMessage::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
conversations
convos
},
errorMapper = { error ->
error?.toDomain()
@@ -109,11 +109,11 @@ class ConversationsRepositoryImpl(
)
}
override suspend fun getConversationsById(
override suspend fun getConvosById(
peerIds: List<Long>,
extended: Boolean?,
fields: String?
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
): ApiResult<List<VkConvo>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestParams = mutableMapOf(
"peer_ids" to peerIds.joinToString(separator = ",")
).apply {
@@ -121,7 +121,7 @@ class ConversationsRepositoryImpl(
fields?.let { this["fields"] = it }
}
conversationsService.getConversationsById(requestParams).mapApiResult(
convosService.getConvosById(requestParams).mapApiResult(
successMapper = { apiResponse ->
val response = apiResponse.requireResponse()
@@ -132,17 +132,17 @@ class ConversationsRepositoryImpl(
val usersMap = VkUsersMap.forUsers(profilesList)
val groupsMap = VkGroupsMap.forGroups(groupsList)
val conversations = response.items.map { item ->
item.asDomain().let { conversation ->
conversation.copy(
user = usersMap.conversationUser(conversation),
group = groupsMap.conversationGroup(conversation)
).also { VkMemoryCache[conversation.id] = it }
val convos = response.items.map { item ->
item.asDomain().let { convo ->
convo.copy(
user = usersMap.convoUser(convo),
group = groupsMap.convoGroup(convo)
).also { VkMemoryCache[convo.id] = it }
}
}
launch(Dispatchers.IO) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
convoDao.insertAll(convos.map(VkConvo::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
@@ -151,7 +151,7 @@ class ConversationsRepositoryImpl(
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
conversations
convos
},
errorMapper = { error ->
error?.toDomain()
@@ -161,7 +161,7 @@ class ConversationsRepositoryImpl(
override suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain> =
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 },
errorMapper = { error -> error?.toDomain() }
)
@@ -170,19 +170,19 @@ class ConversationsRepositoryImpl(
override suspend fun pin(
peerId: Long
): 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(
peerId: Long
): 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(
peerIds: List<Long>
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
conversationsService
convosService
.reorderPinned(mapOf("peer_ids" to peerIds.joinToString(",")))
.mapApiDefault()
}
@@ -190,12 +190,12 @@ class ConversationsRepositoryImpl(
override suspend fun archive(
peerId: Long
): 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(
peerId: Long
): 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
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkMessage
data class MessagesHistoryInfo(
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.VkAttachmentHistoryMessage
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.MessagesSendResponse
import dev.meloda.fast.network.RestApiErrorDomain
@@ -15,7 +15,7 @@ interface MessagesRepository {
suspend fun storeMessages(messages: List<VkMessage>)
suspend fun getHistory(
conversationId: Long,
convoId: Long,
offset: Int?,
count: Int?
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain>
@@ -99,13 +99,13 @@ interface MessagesRepository {
fields: String? = null
): ApiResult<VkChatData, RestApiErrorDomain>
suspend fun getConversationMembers(
suspend fun getConvoMembers(
peerId: Long,
offset: Int? = null,
count: Int? = null,
extended: Boolean? = null,
fields: String? = null
): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain>
): ApiResult<MessagesGetConvoMembersResponse, RestApiErrorDomain>
suspend fun removeChatUser(
chatId: Long,
@@ -5,7 +5,7 @@ import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.data.VkGroupsMap
import dev.meloda.fast.data.VkMemoryCache
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.MessageDao
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.domain.VkAttachment
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.VkMessage
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.MessagesGetByIdRequest
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.MessagesGetHistoryRequest
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.MessagesSendRequest
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.MessagesSendResponse
import dev.meloda.fast.network.RestApiErrorDomain
@@ -52,18 +52,18 @@ class MessagesRepositoryImpl(
private val messageDao: MessageDao,
private val userDao: UserDao,
private val groupDao: GroupDao,
private val conversationDao: ConversationDao
private val convoDao: ConvoDao
) : MessagesRepository {
override suspend fun getHistory(
conversationId: Long,
convoId: Long,
offset: Int?,
count: Int?
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesGetHistoryRequest(
count = count,
offset = offset,
peerId = conversationId,
peerId = convoId,
extended = true,
startMessageId = 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 }
item.asDomain(message)
.let { conversation ->
conversation.copy(
user = usersMap.conversationUser(conversation),
group = groupsMap.conversationGroup(conversation)
).also { VkMemoryCache[conversation.id] = it }
.let { convo ->
convo.copy(
user = usersMap.convoUser(convo),
group = groupsMap.convoGroup(convo)
).also { VkMemoryCache[convo.id] = it }
}
}
launch(Dispatchers.IO) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
convoDao.insertAll(convos.map(VkConvo::asEntity))
messageDao.insertAll(messages.map(VkMessage::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
@@ -124,7 +124,7 @@ class MessagesRepositoryImpl(
MessagesHistoryInfo(
messages = messages,
conversations = conversations
convos = convos
)
},
errorMapper = { error ->
@@ -243,7 +243,7 @@ class MessagesRepositoryImpl(
offset = offset,
preserveOrder = true,
attachmentTypes = attachmentTypes,
conversationMessageId = cmId,
cmId = cmId,
fields = VkConstants.ALL_FIELDS
)
@@ -297,7 +297,7 @@ class MessagesRepositoryImpl(
val requestModel = MessagesPinMessageRequest(
peerId = peerId,
messageId = messageId,
conversationMessageId = cmId
cmId = cmId
)
messagesService.pin(requestModel.map).mapApiResult(
@@ -343,7 +343,7 @@ class MessagesRepositoryImpl(
val requestModel = MessagesDeleteRequest(
peerId = peerId,
messagesIds = messageIds,
conversationsMessagesIds = cmIds,
cmIds = cmIds,
isSpam = spam,
deleteForAll = deleteForAll
)
@@ -394,15 +394,15 @@ class MessagesRepositoryImpl(
messagesService.getChat(requestModel.map).mapApiDefault()
}
override suspend fun getConversationMembers(
override suspend fun getConvoMembers(
peerId: Long,
offset: Int?,
count: Int?,
extended: Boolean?,
fields: String?
): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain> =
): ApiResult<MessagesGetConvoMembersResponse, RestApiErrorDomain> =
withContext(Dispatchers.IO) {
val requestModel = MessagesGetConversationMembersRequest(
val requestModel = MessagesGetConvoMembersRequest(
peerId = peerId,
offset = offset,
count = count,
@@ -410,7 +410,7 @@ class MessagesRepositoryImpl(
fields = fields
)
messagesService.getConversationMembers(requestModel.map).mapApiDefault()
messagesService.getConvoMembers(requestModel.map).mapApiDefault()
}
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.auth.AuthRepository
import dev.meloda.fast.data.api.auth.AuthRepositoryImpl
import dev.meloda.fast.data.api.conversations.ConversationsRepository
import dev.meloda.fast.data.api.conversations.ConversationsRepositoryImpl
import dev.meloda.fast.data.api.convos.ConvosRepository
import dev.meloda.fast.data.api.convos.ConvosRepositoryImpl
import dev.meloda.fast.data.api.files.FilesRepository
import dev.meloda.fast.data.api.friends.FriendsRepository
import dev.meloda.fast.data.api.friends.FriendsRepositoryImpl
@@ -45,7 +45,7 @@ val dataModule = module {
singleOf(::AuthRepositoryImpl) bind AuthRepository::class
singleOf(::ConversationsRepositoryImpl) bind ConversationsRepository::class
singleOf(::ConvosRepositoryImpl) bind ConvosRepository::class
singleOf(::FilesRepository)
@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "fa307a5eb2e1f7d601bd1374174635cd",
"identityHash": "6c315b7f800694f635318d86032746ec",
"entities": [
{
"tableName": "users",
@@ -41,50 +41,42 @@
{
"fieldPath": "onlineAppId",
"columnName": "onlineAppId",
"affinity": "INTEGER",
"notNull": false
"affinity": "INTEGER"
},
{
"fieldPath": "lastSeen",
"columnName": "lastSeen",
"affinity": "INTEGER",
"notNull": false
"affinity": "INTEGER"
},
{
"fieldPath": "lastSeenStatus",
"columnName": "lastSeenStatus",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "birthday",
"columnName": "birthday",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "photo400Orig",
"columnName": "photo400Orig",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
}
],
"primaryKey": {
@@ -92,9 +84,7 @@
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
},
{
"tableName": "groups",
@@ -121,26 +111,22 @@
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER",
"notNull": false
"affinity": "INTEGER"
}
],
"primaryKey": {
@@ -148,13 +134,11 @@
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
},
{
"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": [
{
"fieldPath": "id",
@@ -163,16 +147,15 @@
"notNull": true
},
{
"fieldPath": "conversationMessageId",
"columnName": "conversationMessageId",
"fieldPath": "cmId",
"columnName": "cmId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "text",
"columnName": "text",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "isOut",
@@ -207,38 +190,32 @@
{
"fieldPath": "action",
"columnName": "action",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "actionMemberId",
"columnName": "actionMemberId",
"affinity": "INTEGER",
"notNull": false
"affinity": "INTEGER"
},
{
"fieldPath": "actionText",
"columnName": "actionText",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "actionConversationMessageId",
"columnName": "actionConversationMessageId",
"affinity": "INTEGER",
"notNull": false
"fieldPath": "actionCmId",
"columnName": "actionCmId",
"affinity": "INTEGER"
},
{
"fieldPath": "actionMessage",
"columnName": "actionMessage",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "updateTime",
"columnName": "updateTime",
"affinity": "INTEGER",
"notNull": false
"affinity": "INTEGER"
},
{
"fieldPath": "important",
@@ -249,32 +226,27 @@
{
"fieldPath": "forwardIds",
"columnName": "forwardIds",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "attachments",
"columnName": "attachments",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "replyMessageId",
"columnName": "replyMessageId",
"affinity": "INTEGER",
"notNull": false
"affinity": "INTEGER"
},
{
"fieldPath": "geoType",
"columnName": "geoType",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "pinnedAt",
"columnName": "pinnedAt",
"affinity": "INTEGER",
"notNull": false
"affinity": "INTEGER"
},
{
"fieldPath": "isPinned",
@@ -288,13 +260,11 @@
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
},
{
"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": [
{
"fieldPath": "id",
@@ -311,32 +281,27 @@
{
"fieldPath": "ownerId",
"columnName": "ownerId",
"affinity": "INTEGER",
"notNull": false
"affinity": "INTEGER"
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "photo50",
"columnName": "photo50",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "photo100",
"columnName": "photo100",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "photo200",
"columnName": "photo200",
"affinity": "TEXT",
"notNull": false
"affinity": "TEXT"
},
{
"fieldPath": "isPhantom",
@@ -345,8 +310,8 @@
"notNull": true
},
{
"fieldPath": "lastConversationMessageId",
"columnName": "lastConversationMessageId",
"fieldPath": "lastCmId",
"columnName": "lastCmId",
"affinity": "INTEGER",
"notNull": true
},
@@ -377,8 +342,7 @@
{
"fieldPath": "lastMessageId",
"columnName": "lastMessageId",
"affinity": "INTEGER",
"notNull": false
"affinity": "INTEGER"
},
{
"fieldPath": "unreadCount",
@@ -389,8 +353,7 @@
{
"fieldPath": "membersCount",
"columnName": "membersCount",
"affinity": "INTEGER",
"notNull": false
"affinity": "INTEGER"
},
{
"fieldPath": "canChangePin",
@@ -419,8 +382,7 @@
{
"fieldPath": "pinnedMessageId",
"columnName": "pinnedMessageId",
"affinity": "INTEGER",
"notNull": false
"affinity": "INTEGER"
},
{
"fieldPath": "peerType",
@@ -440,15 +402,12 @@
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
}
],
"views": [],
"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, '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.RoomDatabase
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.MessageDao
import dev.meloda.fast.database.dao.UserDao
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.VkMessageEntity
import dev.meloda.fast.model.database.VkUserEntity
@@ -18,15 +18,15 @@ import dev.meloda.fast.model.database.VkUserEntity
VkUserEntity::class,
VkGroupEntity::class,
VkMessageEntity::class,
VkConversationEntity::class
VkConvoEntity::class
],
version = 10
version = 11
)
@TypeConverters(Converters::class)
abstract class CacheDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun groupDao(): GroupDao
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")
abstract suspend fun getAll(): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE peerId IS (:conversationId)")
abstract suspend fun getAll(conversationId: Long): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE peerId IS (:convoId)")
abstract suspend fun getAll(convoId: Long): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE id IN (:ids)")
abstract suspend fun getAllByIds(ids: List<Int>): List<VkMessageEntity>
@@ -23,7 +23,7 @@ val databaseModule = module {
single { cacheDB().userDao() }
single { cacheDB().groupDao() }
single { cacheDB().messageDao() }
single { cacheDB().conversationDao() }
single { cacheDB().convoDao() }
}
private fun Scope.cacheDB(): CacheDatabase = get()
+5
View File
@@ -8,6 +8,7 @@ android {
}
dependencies {
api(projects.core.common)
api(projects.core.data)
api(projects.core.model)
@@ -15,4 +16,8 @@ dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.koin.core)
implementation(libs.eithernet)
implementation(libs.bundles.nanokt)
implementation(libs.compose.ui)
}
@@ -1,25 +1,25 @@
package dev.meloda.fast.domain
import dev.meloda.fast.data.State
import dev.meloda.fast.model.ConversationsFilter
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.domain.VkConvo
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,
offset: Int? = null,
filter: ConversationsFilter
): Flow<State<List<VkConversation>>>
filter: ConvosFilter
): Flow<State<List<VkConvo>>>
fun getById(
peerIds: List<Long>,
extended: Boolean? = null,
fields: String? = null
): Flow<State<List<VkConversation>>>
): Flow<State<List<VkConvo>>>
fun delete(peerId: Long): Flow<State<Long>>
@@ -1,30 +1,30 @@
package dev.meloda.fast.domain
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.model.ConversationsFilter
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.domain.VkConvo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class ConversationsUseCaseImpl(
private val repository: ConversationsRepository,
) : ConversationsUseCase {
class ConvoUseCaseImpl(
private val repository: ConvosRepository,
) : ConvoUseCase {
override suspend fun storeConversations(
conversations: List<VkConversation>
override suspend fun storeConvos(
convos: List<VkConvo>
) = withContext(Dispatchers.IO) {
repository.storeConversations(conversations)
repository.storeConvos(convos)
}
override fun getConversations(
override fun getConvos(
count: Int?,
offset: Int?,
filter: ConversationsFilter
): Flow<State<List<VkConversation>>> = flowNewState {
repository.getConversations(
filter: ConvosFilter
): Flow<State<List<VkConvo>>> = flowNewState {
repository.getConvos(
count = count,
offset = offset,
filter = filter
@@ -35,8 +35,8 @@ class ConversationsUseCaseImpl(
peerIds: List<Long>,
extended: Boolean?,
fields: String?
): Flow<State<List<VkConversation>>> = flowNewState {
repository.getConversationsById(
): Flow<State<List<VkConvo>>> = flowNewState {
repository.getConvosById(
peerIds = peerIds,
extended = extended,
fields = fields
@@ -1,22 +1,22 @@
package dev.meloda.fast.domain
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.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkConvo
import kotlinx.coroutines.flow.Flow
class LoadConversationsByIdUseCase(
private val conversationsRepository: ConversationsRepository
class LoadConvosByIdUseCase(
private val convosRepository: ConvosRepository
) : BaseUseCase {
operator fun invoke(
peerIds: List<Long>,
extended: Boolean? = null,
fields: String? = null
): Flow<State<List<VkConversation>>> = flowNewState {
conversationsRepository
.getConversationsById(
): Flow<State<List<VkConvo>>> = flowNewState {
convosRepository
.getConvosById(
peerIds = peerIds,
extended = extended,
fields = fields,
@@ -9,12 +9,12 @@ import dev.meloda.fast.common.extensions.toList
import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.processState
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.LongPollEvent
import dev.meloda.fast.model.LongPollParsedEvent
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 kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
@@ -28,7 +28,7 @@ import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class LongPollUpdatesParser(
private val conversationsUseCase: ConversationsUseCase,
private val convoUseCase: ConvoUseCase,
private val messagesUseCase: MessagesUseCase
) {
private val job = SupervisorJob()
@@ -271,9 +271,9 @@ class LongPollUpdatesParser(
val message =
async { loadMessage(peerId = peerId, cmId = cmId) }.await()
val conversation =
val convo =
async {
loadConversation(
loadConvo(
peerId = peerId,
extended = true,
fields = VkConstants.ALL_FIELDS
@@ -287,7 +287,7 @@ class LongPollUpdatesParser(
.onEvent(
LongPollParsedEvent.NewMessage(
message = message,
inArchive = conversation?.isArchived == true
inArchive = convo?.isArchived == true
// TODO: 03-Apr-25, Danil Nikolaev:
// load user settings about restoring chats with
// enabled notifications from archive
@@ -368,13 +368,13 @@ class LongPollUpdatesParser(
val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = ConversationFlags.parse(flags)
val parsedFlags = ConvoFlags.parse(flags)
coroutineScope.launch(Dispatchers.IO) {
parsedFlags.forEach { flag ->
when (flag) {
ConversationFlags.ARCHIVED -> {
val conversation = loadConversation(
ConvoFlags.ARCHIVED -> {
val convo = loadConvo(
peerId = peerId,
extended = true,
fields = VkConstants.ALL_FIELDS
@@ -382,11 +382,11 @@ class LongPollUpdatesParser(
val message = loadMessage(
peerId = peerId,
cmId = conversation.lastCmId
cmId = convo.lastCmId
)
val eventToSend = LongPollParsedEvent.ChatArchived(
conversation = conversation.copy(lastMessage = message),
convo = convo.copy(lastMessage = message),
archived = false
)
eventsToSend += eventToSend
@@ -423,13 +423,13 @@ class LongPollUpdatesParser(
val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = ConversationFlags.parse(flags)
val parsedFlags = ConvoFlags.parse(flags)
coroutineScope.launch(Dispatchers.IO) {
parsedFlags.forEach { flag ->
when (flag) {
ConversationFlags.ARCHIVED -> {
val conversation = loadConversation(
ConvoFlags.ARCHIVED -> {
val convo = loadConvo(
peerId = peerId,
extended = true,
fields = VkConstants.ALL_FIELDS
@@ -437,11 +437,11 @@ class LongPollUpdatesParser(
val message = loadMessage(
peerId = peerId,
cmId = conversation.lastCmId
cmId = convo.lastCmId
)
val eventToSend = LongPollParsedEvent.ChatArchived(
conversation = conversation.copy(lastMessage = message),
convo = convo.copy(lastMessage = message),
archived = true
)
eventsToSend += eventToSend
@@ -673,29 +673,29 @@ class LongPollUpdatesParser(
}
}
private suspend fun loadConversation(
private suspend fun loadConvo(
peerId: Long,
extended: Boolean = false,
fields: String? = null
): VkConversation? = suspendCoroutine { continuation ->
): VkConvo? = suspendCoroutine { continuation ->
coroutineScope.launch(Dispatchers.IO) {
conversationsUseCase.getById(
convoUseCase.getById(
peerIds = listOf(peerId),
extended = extended,
fields = fields
).listenValue(coroutineScope) { state ->
state.processState(
error = { error ->
Log.e("LongPollUpdatesParser", "loadConversation: error: $error")
Log.e("LongPollUpdatesParser", "loadConvo: error: $error")
continuation.resume(null)
},
success = { response ->
val conversation = response.singleOrNull() ?: run {
val convo = response.singleOrNull() ?: run {
continuation.resume(null)
return@listenValue
}
continuation.resume(conversation)
continuation.resume(convo)
}
)
}
@@ -14,7 +14,7 @@ interface MessagesUseCase : BaseUseCase {
suspend fun storeMessages(messages: List<VkMessage>)
fun getMessagesHistory(
conversationId: Long,
convoId: Long,
count: Int?,
offset: Int?
): Flow<State<MessagesHistoryInfo>>
@@ -23,12 +23,12 @@ class MessagesUseCaseImpl(
}
override fun getMessagesHistory(
conversationId: Long,
convoId: Long,
count: Int?,
offset: Int?
): Flow<State<MessagesHistoryInfo>> = flowNewState {
repository.getHistory(
conversationId = conversationId,
convoId = convoId,
offset = offset,
count = count
).mapToState()
@@ -7,7 +7,7 @@ import dev.meloda.fast.domain.GetCurrentAccountUseCase
import dev.meloda.fast.domain.GetLocalUserByIdUseCase
import dev.meloda.fast.domain.GetLocalUsersByIdsUseCase
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.LoadUsersByIdsUseCase
import dev.meloda.fast.domain.StoreUsersUseCase
@@ -27,7 +27,7 @@ val domainModule = module {
singleOf(::AccountUseCaseImpl) bind AccountUseCase::class
singleOf(::GetCurrentAccountUseCase)
singleOf(::LoadConversationsByIdUseCase)
singleOf(::LoadConvosByIdUseCase)
singleOf(::GetMessageReadPeersUseCase)
}
@@ -0,0 +1,772 @@
package dev.meloda.fast.domain.util
import android.content.res.Resources
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.withStyle
import com.conena.nanokt.jvm.util.dayOfMonth
import com.conena.nanokt.jvm.util.month
import dev.meloda.fast.common.extensions.orDots
import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.common.model.UiText
import dev.meloda.fast.common.model.parseString
import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.model.InteractionType
import dev.meloda.fast.model.api.PeerType
import dev.meloda.fast.model.api.data.AttachmentType
import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.domain.VkVideoDomain
import dev.meloda.fast.ui.R
import java.util.Calendar
import java.util.Locale
import kotlin.math.ln
import kotlin.math.pow
fun VkConvo.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 VkConvo.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 extractUnreadCount(
lastMessage: VkMessage?,
convo: VkConvo
): String? = when {
lastMessage?.isOut == false && convo.isInRead() -> null
convo.unreadCount == 0 -> null
convo.unreadCount < 1000 -> convo.unreadCount.toString()
else -> {
val exp = (ln(convo.unreadCount.toDouble()) / ln(1000.0)).toInt()
val suffix = "KMBT"[exp - 1]
val result = convo.unreadCount / 1000.0.pow(exp.toDouble())
if (result.toLong().toDouble() == result) {
String.format(Locale.getDefault(), "%.0f%s", result, suffix)
} else {
String.format(Locale.getDefault(), "%.1f%s", result, suffix)
}
}
}
fun extractMessage(
resources: Resources,
lastMessage: VkMessage?,
peerId: Long,
peerType: PeerType,
showPeer: Boolean = true
): AnnotatedString {
val youPrefix = UiText.Resource(R.string.you_message_prefix)
.parseString(resources)
.orDots()
val actionMessage = extractActionText(
lastMessage = lastMessage,
resources = resources,
youPrefix = youPrefix
)
val attachmentIcon: UiImage? = extractAttachmentIcon(lastMessage)
val attachmentText: AnnotatedString? =
if (attachmentIcon != null) null
else extractAttachmentText(resources, lastMessage)
val forwardsMessage =
if (lastMessage?.text != null) null
else extractForwardsText(resources, lastMessage)
val messageText = lastMessage?.text.orEmpty()
val prefixText: AnnotatedString? = when {
!showPeer -> null
actionMessage != null -> null
lastMessage == null -> null
peerId == UserConfig.userId -> null
!peerType.isChat() && !lastMessage.isOut -> null
lastMessage.isOut -> buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) {
append(youPrefix)
}
}
else ->
when {
lastMessage.user?.firstName.orEmpty().isNotEmpty() -> buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) {
append(lastMessage.user?.firstName)
}
}
lastMessage.group?.name.orEmpty().isNotEmpty() -> buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) {
append(lastMessage.group?.name)
}
}
else -> null
}
}
val prefix = buildAnnotatedString {
if (prefixText != null) {
append(prefixText)
append(": ")
}
}
val finalText = when {
actionMessage != null -> {
prefix + actionMessage
}
forwardsMessage != null -> {
prefix + forwardsMessage
}
attachmentText != null -> {
prefix + attachmentText
}
else ->
messageText
.replace("\n", " ")
.replace("&amp;", "&")
.replace("&quot;", "\"")
.replace("<br>", " ")
.replace("&gt;", ">")
.replace("&lt;", "<")
.replace("<br/>", " ")
.replace("&ndash;", "-")
.trim()
.let { text ->
extractTextWithVisualizedMentions(
isOut = lastMessage?.isOut == true,
originalText = text,
formatData = null
)
}
.let { text -> prefix + text.orEmpty() }
}
return finalText
}
fun extractActionText(
lastMessage: VkMessage?,
resources: Resources,
youPrefix: String
): AnnotatedString? {
if (lastMessage == null) return null
val fromId = lastMessage.fromId
val text = lastMessage.actionText.orDots()
val groupName = lastMessage.group?.name.orDots()
val userName = lastMessage.user?.fullName.orDots()
val actionGroupName = lastMessage.actionGroup?.name.orDots()
val actionUserName = lastMessage.actionUser?.fullName.orDots()
val memberId = lastMessage.actionMemberId
val isMemberUser = (memberId ?: 0) > 0
val isMemberGroup = (memberId ?: 0) < 0
val prefix = when {
lastMessage.fromId == UserConfig.userId -> youPrefix
lastMessage.isGroup() -> groupName
lastMessage.isUser() -> userName
else -> null
}.orDots()
val memberPrefix = when {
memberId == UserConfig.userId -> youPrefix
isMemberUser -> actionUserName
isMemberGroup -> actionGroupName
else -> null
}.orDots()
return buildAnnotatedString {
when (lastMessage.action) {
null -> return null
VkMessage.Action.CHAT_CREATE -> {
val string = UiText.ResourceParams(
R.string.message_action_chat_created,
listOf(prefix, text)
).parseString(resources).orEmpty()
append(string)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = prefix.length
)
val textStartIndex = string.indexOf(text)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = textStartIndex,
end = textStartIndex + text.length
)
}
VkMessage.Action.CHAT_TITLE_UPDATE -> {
val string = UiText.ResourceParams(
R.string.message_action_chat_renamed,
listOf(prefix, text)
).parseString(resources).orEmpty()
append(string)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = prefix.length
)
val textStartIndex = string.indexOf(text)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = textStartIndex,
end = textStartIndex + text.length
)
}
VkMessage.Action.CHAT_PHOTO_UPDATE -> {
UiText.ResourceParams(
R.string.message_action_chat_photo_update,
listOf(prefix)
).parseString(resources).orEmpty().let(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_PHOTO_REMOVE -> {
UiText.ResourceParams(
R.string.message_action_chat_photo_remove,
listOf(prefix)
).parseString(resources).orEmpty().let(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_KICK_USER -> {
if (memberId == fromId) {
UiText.ResourceParams(
R.string.message_action_chat_user_left,
listOf(memberPrefix)
).parseString(resources).orEmpty().let(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = memberPrefix.length
)
} else {
val postfix =
if (memberId == UserConfig.userId) youPrefix.lowercase()
else lastMessage.actionUser.toString()
val string = UiText.ResourceParams(
R.string.message_action_chat_user_kicked,
listOf(prefix, postfix)
).parseString(resources).orEmpty()
append(string)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = prefix.length
)
val postfixStartIndex = string.indexOf(postfix)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = postfixStartIndex,
end = postfixStartIndex + postfix.length
)
}
}
VkMessage.Action.CHAT_INVITE_USER -> {
if (memberId == lastMessage.fromId) {
UiText.ResourceParams(
R.string.message_action_chat_user_returned,
listOf(memberPrefix)
).parseString(resources).orEmpty().let(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = memberPrefix.length
)
} else {
val postfix =
if (memberId == UserConfig.userId) youPrefix.lowercase()
else lastMessage.actionUser.toString()
val string = UiText.ResourceParams(
R.string.message_action_chat_user_invited,
listOf(memberPrefix, postfix)
).parseString(resources).orEmpty()
append(string)
val postfixStartIndex = string.indexOf(postfix)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = postfixStartIndex,
end = postfixStartIndex + postfix.length
)
}
}
VkMessage.Action.CHAT_INVITE_USER_BY_LINK -> {
UiText.ResourceParams(
R.string.message_action_chat_user_joined_by_link,
listOf(prefix)
).parseString(resources).orEmpty().let(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_INVITE_USER_BY_CALL -> {
UiText.ResourceParams(
R.string.message_action_chat_user_joined_by_call,
listOf(prefix)
).parseString(resources).orEmpty().let(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> {
UiText.ResourceParams(
R.string.message_action_chat_user_joined_by_call_link,
listOf(prefix)
).parseString(resources).orEmpty().let(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_PIN_MESSAGE -> {
UiText.ResourceParams(
R.string.message_action_chat_pin_message,
listOf(prefix)
).parseString(resources).orEmpty().let(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_UNPIN_MESSAGE -> {
UiText.ResourceParams(
R.string.message_action_chat_unpin_message,
listOf(prefix)
).parseString(resources).orEmpty().let(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_SCREENSHOT -> {
UiText.ResourceParams(
R.string.message_action_chat_screenshot,
listOf(prefix)
).parseString(resources).orEmpty().let(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_STYLE_UPDATE -> {
UiText.ResourceParams(
R.string.message_action_chat_style_update,
listOf(prefix)
).parseString(resources).orEmpty().let(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.SemiBold),
start = 0,
end = prefix.length
)
}
}
}
}
private fun extractAttachmentIcon(
lastMessage: VkMessage?
): UiImage? = when {
lastMessage == null -> null
lastMessage.text == null -> null
!lastMessage.forwards.isNullOrEmpty() -> {
if (lastMessage.forwards.orEmpty().size == 1) {
UiImage.Resource(R.drawable.ic_attachment_forwarded_message)
} else {
UiImage.Resource(R.drawable.ic_attachment_forwarded_messages)
}
}
else -> {
lastMessage.attachments?.let { attachments ->
if (attachments.isEmpty()) return null
if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) {
lastMessage.geoType?.let {
return UiImage.Resource(R.drawable.ic_map_marker)
}
getAttachmentIconByType(attachments.first().type)
} else {
UiImage.Resource(R.drawable.ic_baseline_attach_file_24)
}
}
}
}
fun extractAttachmentText(
resources: Resources,
lastMessage: VkMessage?
): AnnotatedString? = when {
lastMessage == null -> null
lastMessage.geoType != null -> {
buildAnnotatedString {
withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) {
when (lastMessage.geoType) {
"point" -> {
UiText.Resource(R.string.message_geo_point)
.parseString(resources)
.let(::append)
}
else -> {
UiText.Resource(R.string.message_geo)
.parseString(resources)
.let(::append)
}
}
}
}
}
lastMessage.hasAttachments() -> {
buildAnnotatedString {
val attachments = lastMessage.attachments.orEmpty()
withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) {
if (attachments.size == 1) {
getAttachmentUiText(attachments.first())
.parseString(resources)
.let(::append)
} else {
when {
isAttachmentsHaveOneType(attachments) -> {
getAttachmentUiText(attachments.first(), attachments.size)
.parseString(resources)
.let(::append)
}
attachments.any { it.type == AttachmentType.ARTIST } -> {
getAttachmentUiText(
attachments.first { it.type == AttachmentType.ARTIST }
)
.parseString(resources)
.let(::append)
}
else -> {
UiText.Resource(R.string.message_attachments_many)
.parseString(resources)
.let(::append)
}
}
}
}
}
}
else -> null
}
private fun getAttachmentIconByType(attachmentType: AttachmentType): UiImage? {
return when (attachmentType) {
AttachmentType.PHOTO -> R.drawable.ic_attachment_photo
AttachmentType.VIDEO -> R.drawable.ic_attachment_video
AttachmentType.AUDIO -> R.drawable.ic_attachment_audio
AttachmentType.FILE -> R.drawable.ic_attachment_file
AttachmentType.LINK -> R.drawable.ic_attachment_link
AttachmentType.AUDIO_MESSAGE -> R.drawable.ic_attachment_voice
AttachmentType.MINI_APP -> R.drawable.ic_attachment_mini_app
AttachmentType.STICKER -> R.drawable.ic_attachment_sticker
AttachmentType.GIFT -> R.drawable.ic_attachment_gift
AttachmentType.WALL -> R.drawable.ic_attachment_wall
AttachmentType.GRAFFITI -> R.drawable.ic_attachment_graffiti
AttachmentType.POLL -> R.drawable.ic_attachment_poll
AttachmentType.WALL_REPLY -> R.drawable.ic_attachment_wall_reply
AttachmentType.CALL -> R.drawable.ic_attachment_call
AttachmentType.GROUP_CALL_IN_PROGRESS -> R.drawable.ic_attachment_group_call
AttachmentType.STORY -> R.drawable.ic_attachment_story
AttachmentType.UNKNOWN -> null
AttachmentType.CURATOR -> null
AttachmentType.EVENT -> null
AttachmentType.WIDGET -> null
AttachmentType.ARTIST -> null
AttachmentType.AUDIO_PLAYLIST -> null
AttachmentType.PODCAST -> null
AttachmentType.NARRATIVE -> null
AttachmentType.ARTICLE -> null
AttachmentType.VIDEO_MESSAGE -> null
AttachmentType.GROUP_CHAT_STICKER -> R.drawable.ic_attachment_sticker
AttachmentType.STICKER_PACK_PREVIEW -> null
}?.let(UiImage::Resource)
}
private fun isAttachmentsHaveOneType(attachments: List<VkAttachment>): Boolean {
if (attachments.isEmpty()) return true
if (attachments.size == 1) return true
val firstType = attachments.first().type
for (attachment in attachments) {
if (firstType != attachment.type) return false
}
return true
}
fun extractForwardsText(
resources: Resources,
lastMessage: VkMessage?
): AnnotatedString? = when {
lastMessage == null -> null
lastMessage.hasForwards() -> buildAnnotatedString {
val forwards = lastMessage.forwards.orEmpty()
withStyle(style = SpanStyle(fontWeight = FontWeight.SemiBold)) {
append(
UiText.Resource(
if (forwards.size == 1) R.string.forwarded_message
else R.string.forwarded_messages
).parseString(resources)
)
}
}
else -> null
}
fun getAttachmentUiText(
attachment: VkAttachment,
size: Int = 1,
): UiText {
if (attachment.type == AttachmentType.VIDEO &&
(attachment as? VkVideoDomain)?.isShortVideo == true
) {
return UiText.Resource(R.string.message_attachments_clip)
}
if (attachment.type.isMultiple()) {
return when (attachment.type) {
AttachmentType.PHOTO -> R.plurals.attachment_photos
AttachmentType.VIDEO -> R.plurals.attachment_videos
AttachmentType.AUDIO -> R.plurals.attachment_audios
AttachmentType.FILE -> R.plurals.attachment_files
else -> throw IllegalArgumentException("Unknown multiple type: ${attachment.type}")
}.let { resId -> UiText.QuantityResource(resId, size) }
}
return when (attachment.type) {
AttachmentType.UNKNOWN,
AttachmentType.PHOTO,
AttachmentType.VIDEO,
AttachmentType.AUDIO,
AttachmentType.FILE -> {
throw IllegalArgumentException("Unknown multiple type: ${attachment.type}")
}
AttachmentType.LINK -> R.string.message_attachments_link
AttachmentType.AUDIO_MESSAGE -> R.string.message_attachments_audio_message
AttachmentType.MINI_APP -> R.string.message_attachments_mini_app
AttachmentType.STICKER -> R.string.message_attachments_sticker
AttachmentType.GIFT -> R.string.message_attachments_gift
AttachmentType.WALL -> R.string.message_attachments_wall
AttachmentType.GRAFFITI -> R.string.message_attachments_graffiti
AttachmentType.POLL -> R.string.message_attachments_poll
AttachmentType.WALL_REPLY -> R.string.message_attachments_wall_reply
AttachmentType.CALL -> R.string.message_attachments_call
AttachmentType.GROUP_CALL_IN_PROGRESS -> R.string.message_attachments_call_in_progress
AttachmentType.CURATOR -> R.string.message_attachments_curator
AttachmentType.EVENT -> R.string.message_attachments_event
AttachmentType.STORY -> R.string.message_attachments_story
AttachmentType.WIDGET -> R.string.message_attachments_widget
AttachmentType.ARTIST -> R.string.message_attachments_artist
AttachmentType.AUDIO_PLAYLIST -> R.string.message_attachments_audio_playlist
AttachmentType.PODCAST -> R.string.message_attachments_podcast
AttachmentType.NARRATIVE -> R.string.message_attachments_narrative
AttachmentType.ARTICLE -> R.string.message_attachments_article
AttachmentType.VIDEO_MESSAGE -> R.string.message_attachments_video_message
AttachmentType.GROUP_CHAT_STICKER -> R.string.message_attachments_group_sticker
AttachmentType.STICKER_PACK_PREVIEW -> R.string.message_attachments_sticker_pack_preview
}.let(UiText::Resource)
}
fun getAttachmentConvoIcon(message: VkMessage?): UiImage? {
return message?.attachments?.let { attachments ->
if (attachments.isEmpty()) return null
if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) {
message.geoType?.let {
return UiImage.Resource(R.drawable.ic_map_marker)
}
getAttachmentIconByType(attachments.first().type)
} else {
UiImage.Resource(R.drawable.ic_baseline_attach_file_24)
}
}
}
fun extractBirthday(convo: VkConvo): Boolean {
val birthday = convo.user?.birthday ?: return false
val splitBirthday = birthday.split(".").mapNotNull(String::toIntOrNull)
if (splitBirthday.isEmpty()) return false
return if (splitBirthday.size > 1) {
val (day, month) = splitBirthday
val birthdayCalendar = Calendar.getInstance().also { calendar ->
calendar.dayOfMonth = day
calendar.month = month - 1
}
val nowCalendar = Calendar.getInstance()
nowCalendar.dayOfMonth == birthdayCalendar.dayOfMonth &&
nowCalendar.month == birthdayCalendar.month
} else false
}
fun extractReadCondition(
convo: VkConvo,
lastMessage: VkMessage?
): Boolean = !convo.isRead(lastMessage)
fun extractInteractionText(
resources: Resources,
convo: VkConvo
): String? {
val interactionType = InteractionType.parse(convo.interactionType)
val interactiveUsers = extractInteractionUsers(convo)
val typingText =
if (interactionType == null) {
null
} else {
if (!convo.peerType.isChat() && interactiveUsers.size == 1) {
when (interactionType) {
InteractionType.File -> R.string.chat_interaction_uploading_file
InteractionType.Photo -> R.string.chat_interaction_uploading_photo
InteractionType.Typing -> R.string.chat_interaction_typing
InteractionType.Video -> R.string.chat_interaction_uploading_video
InteractionType.VoiceMessage -> R.string.chat_interaction_recording_audio_message
}.let(UiText::Resource)
} else {
if (interactiveUsers.size == 1) {
R.string.chat_interaction_chat_single_typing
} else {
R.string.chat_interaction_chat_typing
}.let { resId ->
UiText.ResourceParams(
resId,
listOf(interactiveUsers.joinToString(separator = ", "))
)
}
}.parseString(resources)
}
return typingText
}
fun extractInteractionUsers(convo: VkConvo): List<String> {
return convo.interactionIds.mapNotNull { id ->
when {
id > 0 -> VkMemoryCache.getUser(id)?.fullName
id < 0 -> VkMemoryCache.getGroup(id)?.name
else -> null
}
}
}
@@ -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.data.VkMemoryCache
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(
useContactNames: Boolean = false
@@ -0,0 +1,461 @@
package dev.meloda.fast.domain.util
import android.content.res.Resources
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import dev.meloda.fast.common.extensions.orDots
import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.common.model.UiText
import dev.meloda.fast.common.model.parseString
import dev.meloda.fast.data.UserConfig
import dev.meloda.fast.model.api.PeerType.Companion.getPeerType
import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.ui.R
import java.text.SimpleDateFormat
import java.util.Locale
fun VkMessage.extractAvatar() = when {
isUser() -> {
if (isAccount(id)) null
else user?.photo200
}
isGroup() -> {
group?.photo200
}
else -> null
}?.let(UiImage::Url) ?: UiImage.Resource(R.drawable.ic_account_circle_cut)
fun VkMessage.extractDate(): String =
SimpleDateFormat("HH:mm", Locale.getDefault()).format(date * 1000L)
fun VkMessage.extractTitle(): String = when {
isUser() -> "%s %s".format(
user?.firstName.orDots(),
user?.lastName?.firstOrNull()?.toString().orEmpty().plus(".")
)
isGroup() -> group?.name.orDots()
else -> throw IllegalStateException("Message is not from user nor group. fromId: $fromId")
}
fun VkMessage.extractReplyTitle(): String? = replyMessage?.extractTitle()
fun VkMessage.extractReplySummary(resources: Resources): AnnotatedString? =
extractMessage(
resources = resources,
lastMessage = this,
peerId = peerId,
peerType = getPeerType(),
showPeer = false
)
fun VkMessage.extractShowAvatar(nextMessage: VkMessage?): Boolean {
if (isOut) return false
return nextMessage == null || nextMessage.fromId != fromId
}
fun VkMessage.extractShowName(prevMessage: VkMessage?): Boolean {
if (isOut || !isPeerChat()) return false
return prevMessage == null || prevMessage.fromId != fromId
}
fun VkMessage.extractActionText(
resources: Resources,
youPrefix: String,
showTime: Boolean
): AnnotatedString? {
val lastMessage = this
val action = lastMessage.action ?: return null
val formattedMessageDate =
SimpleDateFormat("HH:mm", Locale.getDefault()).format(lastMessage.date * 1000L)
val fromId = lastMessage.fromId
val text = lastMessage.actionText.orDots()
val groupName = lastMessage.group?.name.orDots()
val userName = lastMessage.user?.fullName.orDots()
val actionGroupName = lastMessage.actionGroup?.name.orDots()
val actionUserName = lastMessage.actionUser?.fullName.orDots()
val memberId = lastMessage.actionMemberId
val isMemberUser = (memberId ?: 0) > 0
val isMemberGroup = (memberId ?: 0) < 0
val prefix = when {
lastMessage.fromId == UserConfig.userId -> youPrefix
lastMessage.isGroup() -> groupName
lastMessage.isUser() -> userName
else -> null
}.orDots()
val memberPrefix = when {
memberId == UserConfig.userId -> youPrefix
isMemberUser -> actionUserName
isMemberGroup -> actionGroupName
else -> null
}.orDots()
return buildAnnotatedString {
when (action) {
VkMessage.Action.CHAT_CREATE -> {
val string = UiText.ResourceParams(
R.string.message_action_chat_created,
listOf(prefix, text)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = prefix.length
)
val textStartIndex = string.indexOf(text)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = textStartIndex,
end = textStartIndex + text.length
)
}
VkMessage.Action.CHAT_TITLE_UPDATE -> {
val string = UiText.ResourceParams(
R.string.message_action_chat_renamed,
listOf(prefix, text)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = prefix.length
)
val textStartIndex = string.indexOf(text)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = textStartIndex,
end = textStartIndex + text.length
)
}
VkMessage.Action.CHAT_PHOTO_UPDATE -> {
UiText.ResourceParams(
R.string.message_action_chat_photo_update,
listOf(prefix)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_PHOTO_REMOVE -> {
UiText.ResourceParams(
R.string.message_action_chat_photo_remove,
listOf(prefix)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_KICK_USER -> {
if (memberId == fromId) {
UiText.ResourceParams(
R.string.message_action_chat_user_left,
listOf(memberPrefix)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = memberPrefix.length
)
} else {
val postfix =
if (memberId == UserConfig.userId) youPrefix.lowercase()
else lastMessage.actionUser.toString()
val string = UiText.ResourceParams(
R.string.message_action_chat_user_kicked,
listOf(prefix, postfix)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = prefix.length
)
val postfixStartIndex = string.indexOf(postfix)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = postfixStartIndex,
end = postfixStartIndex + postfix.length
)
}
}
VkMessage.Action.CHAT_INVITE_USER -> {
if (memberId == lastMessage.fromId) {
UiText.ResourceParams(
R.string.message_action_chat_user_returned,
listOf(memberPrefix)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = memberPrefix.length
)
} else {
val postfix =
if (memberId == UserConfig.userId) youPrefix.lowercase()
else lastMessage.actionUser.toString()
val string = UiText.ResourceParams(
R.string.message_action_chat_user_invited,
listOf(memberPrefix, postfix)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
val postfixStartIndex = string.indexOf(postfix)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = postfixStartIndex,
end = postfixStartIndex + postfix.length
)
}
}
VkMessage.Action.CHAT_INVITE_USER_BY_LINK -> {
UiText.ResourceParams(
R.string.message_action_chat_user_joined_by_link,
listOf(prefix)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_INVITE_USER_BY_CALL -> {
UiText.ResourceParams(
R.string.message_action_chat_user_joined_by_call,
listOf(prefix)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> {
UiText.ResourceParams(
R.string.message_action_chat_user_joined_by_call_link,
listOf(prefix)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_PIN_MESSAGE -> {
// TODO: 16/07/2024, Danil Nikolaev: get pinned message by cmid
// val messageText = lastMessage.text.orEmpty().trim()
// val croppedMessage = messageText.take(40)
// val hasMessageText = messageText.isNotEmpty()
UiText.ResourceParams(
R.string.message_action_chat_pin_message,
listOf(prefix)
).parseString(resources)
.orEmpty()
// .let { text ->
// if (hasMessageText) {
// text.plus("«%s»".format(croppedMessage))
// .plus(if (messageText.length > 40) "..." else "")
// } else text
// }
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = prefix.length
)
// if (hasMessageText) {
// val croppedIndex = fullText.indexOf(croppedMessage)
//
// addStyle(
// style = SpanStyle(fontWeight = FontWeight.Medium),
// start = croppedIndex - 1,
// end = croppedIndex - 1 + croppedMessage.length + 1
// )
// }
}
VkMessage.Action.CHAT_UNPIN_MESSAGE -> {
UiText.ResourceParams(
R.string.message_action_chat_unpin_message,
listOf(prefix)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_SCREENSHOT -> {
UiText.ResourceParams(
R.string.message_action_chat_screenshot,
listOf(prefix)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = prefix.length
)
}
VkMessage.Action.CHAT_STYLE_UPDATE -> {
UiText.ResourceParams(
R.string.message_action_chat_style_update,
listOf(prefix)
).parseString(resources)
.orEmpty()
.let { text ->
if (showTime) {
text.plus("\n")
.plus(formattedMessageDate)
} else text
}.also(::append)
addStyle(
style = SpanStyle(fontWeight = FontWeight.Medium),
start = 0,
end = prefix.length
)
}
}
}
}
@@ -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
enum class ConversationFlags(val value: Int) {
enum class ConvoFlags(val value: Int) {
DISABLE_PUSH(16),
DISABLE_SOUND(32),
INCOMING_CHAT_REQUEST(256),
@@ -17,10 +17,10 @@ enum class ConversationFlags(val value: Int) {
companion object {
fun parse(mask: Int): List<ConversationFlags> {
val flags = mutableListOf<ConversationFlags>()
fun parse(mask: Int): List<ConvoFlags> {
val flags = mutableListOf<ConvoFlags>()
ConversationFlags.entries.forEach { flag ->
ConvoFlags.entries.forEach { flag ->
if (mask and flag.value > 0) {
flags.add(flag)
}
@@ -1,5 +1,5 @@
package dev.meloda.fast.model
enum class ConversationsFilter {
enum class ConvosFilter {
ALL, UNREAD, ARCHIVE, BUSINESS_NOTIFY
}
@@ -1,6 +1,6 @@
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
sealed interface LongPollParsedEvent {
@@ -92,7 +92,7 @@ sealed interface LongPollParsedEvent {
) : LongPollParsedEvent
data class ChatArchived(
val conversation: VkConversation,
val convo: VkConvo,
val archived: Boolean
) : LongPollParsedEvent
}
@@ -1,5 +1,7 @@
package dev.meloda.fast.model.api
import dev.meloda.fast.model.api.domain.VkMessage
enum class PeerType(val value: String) {
USER("user"),
GROUP("group"),
@@ -13,5 +15,14 @@ enum class PeerType(val value: String) {
fun parse(type: String): PeerType {
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(
@Json(name = "message_id") val messageId: Long,
@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 = "position") val position: Int,
@Json(name = "attachment") val attachment: VkAttachmentItemData
@@ -16,7 +16,7 @@ data class VkAttachmentHistoryMessageData(
fun toDomain(): VkAttachmentHistoryMessage = VkAttachmentHistoryMessage(
messageId = messageId,
conversationMessageId = conversationMessageId,
cmId = cmId,
date = date,
fromId = fromId,
position = position,
@@ -3,19 +3,19 @@ package dev.meloda.fast.model.api.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
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
@JsonClass(generateAdapter = true)
data class VkConversationData(
data class VkConvoData(
@Json(name = "peer") val peer: Peer,
@Json(name = "last_message_id") val lastMessageId: Long?,
@Json(name = "in_read") val inRead: Long,
@Json(name = "out_read") val outRead: Long,
@Json(name = "in_read_cmid") val inReadConversationMessageId: Long,
@Json(name = "out_read_cmid") val outReadConversationMessageId: Long,
@Json(name = "in_read_cmid") val inReadCmId: Long,
@Json(name = "out_read_cmid") val outReadCmId: Long,
@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 = "important") val important: Boolean,
@Json(name = "push_settings") val pushSettings: PushSettings?,
@@ -111,7 +111,7 @@ data class VkConversationData(
fun asDomain(
lastMessage: VkMessage? = null,
): VkConversation = VkConversation(
): VkConvo = VkConvo(
id = peer.id,
localId = peer.localId,
title = chatSettings?.title,
@@ -120,7 +120,7 @@ data class VkConversationData(
photo200 = chatSettings?.photo?.photo200,
isCallInProgress = callInProgress != null,
isPhantom = chatSettings?.isDisappearing == true,
lastCmId = lastConversationMessageId,
lastCmId = lastCmId,
inRead = inRead,
outRead = outRead,
lastMessageId = lastMessageId,
@@ -132,8 +132,8 @@ data class VkConversationData(
canChangePin = chatSettings?.acl?.canChangePin == true,
canChangeInfo = chatSettings?.acl?.canChangeInfo == true,
pinnedMessageId = chatSettings?.pinnedMessage?.id,
inReadCmId = inReadConversationMessageId,
outReadCmId = outReadConversationMessageId,
inReadCmId = inReadCmId,
outReadCmId = outReadCmId,
interactionType = -1,
interactionIds = emptyList(),
peerType = PeerType.parse(peer.type),
@@ -56,7 +56,7 @@ data class VkMessageData(
@Json(name = "type") val type: String,
@Json(name = "member_id") val memberId: Long?,
@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?
)
@@ -102,7 +102,7 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage(
action = VkMessage.Action.parse(action?.type),
actionMemberId = action?.memberId,
actionText = action?.text,
actionConversationMessageId = action?.conversationMessageId,
actionCmId = action?.cmId,
actionMessage = action?.message,
geoType = geo?.type,
isImportant = important == true,
@@ -12,7 +12,7 @@ data class VkPinnedMessageData(
@Json(name = "from_id") val fromId: Long,
@Json(name = "out") val out: Boolean?,
@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 = "important") val important: Boolean = false,
@Json(name = "random_id") val randomId: Long = 0,
@@ -28,7 +28,7 @@ data class VkPinnedMessageData(
fun mapToDomain(): VkMessage = VkMessage(
id = id ?: -1,
cmId = conversationMessageId,
cmId = cmId,
text = text.ifBlank { null },
isOut = out == true,
peerId = peerId ?: -1,
@@ -38,7 +38,7 @@ data class VkPinnedMessageData(
action = VkMessage.Action.parse(action?.type),
actionMemberId = action?.memberId,
actionText = action?.text,
actionConversationMessageId = action?.conversationMessageId,
actionCmId = action?.cmId,
actionMessage = action?.message,
geoType = geo?.type,
isImportant = important,
@@ -2,7 +2,7 @@ package dev.meloda.fast.model.api.domain
data class VkAttachmentHistoryMessage(
val messageId: Long,
val conversationMessageId: Long,
val cmId: Long,
val date: Int,
val fromId: Long,
val position: Int,
@@ -1,9 +1,9 @@
package dev.meloda.fast.model.api.domain
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 localId: Long,
val ownerId: Long?,
@@ -54,7 +54,7 @@ data class VkConversation(
}
companion object {
val EMPTY: VkConversation = VkConversation(
val EMPTY: VkConvo = VkConvo(
id = -1,
localId = -1,
ownerId = null,
@@ -90,7 +90,7 @@ data class VkConversation(
}
}
fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity(
fun VkConvo.asEntity(): VkConvoEntity = VkConvoEntity(
id = id,
localId = localId,
ownerId = ownerId,
@@ -99,7 +99,7 @@ fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity(
photo100 = photo100,
photo200 = photo200,
isPhantom = isPhantom,
lastConversationMessageId = lastCmId,
lastCmId = lastCmId,
inReadCmId = inReadCmId,
outReadCmId = outReadCmId,
inRead = inRead,
@@ -16,7 +16,7 @@ data class VkMessage(
val action: Action?,
val actionMemberId: Long?,
val actionText: String?,
val actionConversationMessageId: Long?,
val actionCmId: Long?,
val actionMessage: String?,
val updateTime: Int?,
@@ -44,9 +44,9 @@ data class VkMessage(
fun isGroup() = fromId < 0
fun isRead(conversation: VkConversation): Boolean = when {
fun isRead(convo: VkConvo): Boolean = when {
id <= 0 -> false
else -> conversation.isRead(this)
else -> convo.isRead(this)
}
fun hasAttachments(): Boolean = attachments.orEmpty().isNotEmpty()
@@ -98,7 +98,7 @@ data class VkMessage(
fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity(
id = id,
conversationMessageId = cmId,
cmId = cmId,
text = text,
isOut = isOut,
peerId = peerId,
@@ -108,7 +108,7 @@ fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity(
action = action?.value,
actionMemberId = actionMemberId,
actionText = actionText,
actionConversationMessageId = actionConversationMessageId,
actionCmId = actionCmId,
actionMessage = actionMessage,
updateTime = updateTime,
important = isImportant,
@@ -1,12 +1,12 @@
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 offset: Int? = null,
val fields: String = "",
val filter: ConversationsFilter = ConversationsFilter.ALL,
val filter: ConvosFilter = ConvosFilter.ALL,
val extended: Boolean? = true,
val startMessageId: Long? = null
) {
@@ -115,7 +115,7 @@ data class MessagesGetLongPollServerRequest(
data class MessagesPinMessageRequest(
val peerId: Long,
val messageId: Long? = null,
val conversationMessageId: Long? = null
val cmId: Long? = null
) {
val map: Map<String, String>
@@ -123,7 +123,7 @@ data class MessagesPinMessageRequest(
"peer_id" to peerId.toString()
).apply {
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(
val peerId: Long,
val messagesIds: List<Long>? = null,
val conversationsMessagesIds: List<Long>? = null,
val cmIds: List<Long>? = null,
val isSpam: Boolean? = null,
val deleteForAll: Boolean? = null
) {
@@ -149,7 +149,7 @@ data class MessagesDeleteRequest(
deleteForAll?.let { this["delete_for_all"] = it.asInt().toString() }
messagesIds?.let { this["message_ids"] = it.joinToString() }
conversationsMessagesIds?.let {
cmIds?.let {
this["conversation_message_ids"] = it.joinToString()
}
}
@@ -228,7 +228,7 @@ data class MessagesGetChatRequest(
}
data class MessagesGetConversationMembersRequest(
data class MessagesGetConvoMembersRequest(
val peerId: Long,
val offset: Int? = null,
val count: Int? = null,
@@ -267,14 +267,14 @@ data class MessagesGetHistoryAttachmentsRequest(
val offset: Int?,
val preserveOrder: Boolean?,
val attachmentTypes: List<String>,
val conversationMessageId: Long,
val cmId: Long,
val fields: String?
) {
val map = mutableMapOf(
"peer_id" to peerId.toString(),
"attachment_types" to attachmentTypes.joinToString(","),
"cmid" to conversationMessageId.toString()
"cmid" to cmId.toString()
).apply {
extended?.let { this["extended"] = 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.JsonClass
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.VkMessageData
import dev.meloda.fast.model.api.data.VkUserData
@JsonClass(generateAdapter = true)
data class ConversationsGetResponse(
data class ConvosGetResponse(
@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 = "profiles") val profiles: List<VkUserData>?,
@Json(name = "groups") val groups: List<VkGroupData>?,
@@ -19,21 +19,21 @@ data class ConversationsGetResponse(
)
@JsonClass(generateAdapter = true)
data class ConversationsGetByIdResponse(
data class ConvosGetByIdResponse(
@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 = "groups") val groups: List<VkGroupData>?,
@Json(name = "contacts") val contacts: List<VkContactData>?
)
@JsonClass(generateAdapter = true)
data class ConversationsResponseItem(
@Json(name = "conversation") val conversation: VkConversationData,
data class ConvosResponseItem(
@Json(name = "conversation") val convo: VkConvoData,
@Json(name = "last_message") val lastMessage: VkMessageData?
)
@JsonClass(generateAdapter = true)
data class ConversationsDeleteResponse(
data class ConvosDeleteResponse(
@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.VkChatMemberData
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.VkMessageData
import dev.meloda.fast.model.api.data.VkUserData
@@ -14,7 +14,7 @@ import dev.meloda.fast.model.api.data.VkUserData
data class MessagesGetHistoryResponse(
val count: Int,
val items: List<VkMessageData>,
val conversations: List<VkConversationData>?,
val convos: List<VkConvoData>?,
val profiles: List<VkUserData>?,
val groups: List<VkGroupData>?,
val contacts: List<VkContactData>?
@@ -30,7 +30,7 @@ data class MessagesGetByIdResponse(
)
@JsonClass(generateAdapter = true)
data class MessagesGetConversationMembersResponse(
data class MessagesGetConvoMembersResponse(
val count: Int,
val items: List<VkChatMemberData>?,
val profiles: List<VkUserData>?,
@@ -3,8 +3,8 @@ package dev.meloda.fast.model.database
import androidx.room.Embedded
import androidx.room.Relation
data class ConversationWithMessage(
@Embedded val conversation: VkConversationEntity,
data class ConvoWithMessage(
@Embedded val convo: VkConvoEntity,
@Relation(
parentColumn = "lastMessageId",
entityColumn = "id"
@@ -3,10 +3,10 @@ package dev.meloda.fast.model.database
import androidx.room.Entity
import androidx.room.PrimaryKey
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")
data class VkConversationEntity(
@Entity(tableName = "convos")
data class VkConvoEntity(
@PrimaryKey val id: Long,
val localId: Long,
val ownerId: Long?,
@@ -15,7 +15,7 @@ data class VkConversationEntity(
val photo100: String?,
val photo200: String?,
val isPhantom: Boolean,
val lastConversationMessageId: Long,
val lastCmId: Long,
val inReadCmId: Long,
val outReadCmId: Long,
val inRead: Long,
@@ -32,7 +32,7 @@ data class VkConversationEntity(
val isArchived: Boolean
)
fun VkConversationEntity.asExternalModel(): VkConversation = VkConversation(
fun VkConvoEntity.asExternalModel(): VkConvo = VkConvo(
id = id,
localId = localId,
ownerId = ownerId,
@@ -42,7 +42,7 @@ fun VkConversationEntity.asExternalModel(): VkConversation = VkConversation(
photo200 = photo200,
isCallInProgress = false,
isPhantom = isPhantom,
lastCmId = lastConversationMessageId,
lastCmId = lastCmId,
inReadCmId = inReadCmId,
outReadCmId = outReadCmId,
inRead = inRead,
@@ -8,7 +8,7 @@ import dev.meloda.fast.model.api.domain.VkUnknownAttachment
@Entity(tableName = "messages")
data class VkMessageEntity(
@PrimaryKey val id: Long,
val conversationMessageId: Long,
val cmId: Long,
val text: String?,
val isOut: Boolean,
val peerId: Long,
@@ -18,7 +18,7 @@ data class VkMessageEntity(
val action: String?,
val actionMemberId: Long?,
val actionText: String?,
val actionConversationMessageId: Long?,
val actionCmId: Long?,
val actionMessage: String?,
val updateTime: Int?,
val important: Boolean,
@@ -32,7 +32,7 @@ data class VkMessageEntity(
fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage(
id = id,
cmId = conversationMessageId,
cmId = cmId,
text = text,
isOut = isOut,
peerId = peerId,
@@ -42,7 +42,7 @@ fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage(
action = VkMessage.Action.parse(action),
actionMemberId = actionMemberId,
actionText = actionText,
actionConversationMessageId = actionConversationMessageId,
actionCmId = actionCmId,
actionMessage = actionMessage,
updateTime = updateTime,
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.audios.AudiosService
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.friends.FriendsService
import dev.meloda.fast.network.service.longpoll.LongPollService
@@ -80,7 +80,7 @@ val networkModule = module {
single { service(AccountService::class.java) }
single { service(AudiosService::class.java) }
single { service(ConversationsService::class.java) }
single { service(ConvosService::class.java) }
single { service(FilesService::class.java) }
single { service(LongPollService::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 dev.meloda.fast.model.api.responses.ConversationsDeleteResponse
import dev.meloda.fast.model.api.responses.ConversationsGetByIdResponse
import dev.meloda.fast.model.api.responses.ConversationsGetResponse
import dev.meloda.fast.model.api.responses.ConvosDeleteResponse
import dev.meloda.fast.model.api.responses.ConvosGetByIdResponse
import dev.meloda.fast.model.api.responses.ConvosGetResponse
import dev.meloda.fast.network.ApiResponse
import dev.meloda.fast.network.RestApiError
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface ConversationsService {
interface ConvosService {
@FormUrlEncoded
@POST(ConversationsUrls.GET)
suspend fun getConversations(
@POST(ConvosUrls.GET)
suspend fun getConvos(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<ConversationsGetResponse>, RestApiError>
): ApiResult<ApiResponse<ConvosGetResponse>, RestApiError>
@FormUrlEncoded
@POST(ConversationsUrls.GET_BY_ID)
suspend fun getConversationsById(
@POST(ConvosUrls.GET_BY_ID)
suspend fun getConvosById(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<ConversationsGetByIdResponse>, RestApiError>
): ApiResult<ApiResponse<ConvosGetByIdResponse>, RestApiError>
@FormUrlEncoded
@POST(ConversationsUrls.DELETE)
@POST(ConvosUrls.DELETE)
suspend fun delete(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<ConversationsDeleteResponse>, RestApiError>
): ApiResult<ApiResponse<ConvosDeleteResponse>, RestApiError>
@FormUrlEncoded
@POST(ConversationsUrls.PIN)
@POST(ConvosUrls.PIN)
suspend fun pin(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(ConversationsUrls.UNPIN)
@POST(ConvosUrls.UNPIN)
suspend fun unpin(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(ConversationsUrls.REORDER_PINNED)
@POST(ConvosUrls.REORDER_PINNED)
suspend fun reorderPinned(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(ConversationsUrls.ARCHIVE)
@POST(ConvosUrls.ARCHIVE)
suspend fun archive(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(ConversationsUrls.UNARCHIVE)
@POST(ConvosUrls.UNARCHIVE)
suspend fun unarchive(
@FieldMap params: Map<String, String>
): 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
object ConversationsUrls {
object ConvosUrls {
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.responses.MessagesCreateChatResponse
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.MessagesGetHistoryResponse
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
@@ -99,10 +99,10 @@ interface MessagesService {
): ApiResult<ApiResponse<VkChatData>, RestApiError>
@FormUrlEncoded
@POST(MessagesUrls.GET_CONVERSATIONS_MEMBERS)
suspend fun getConversationMembers(
@POST(MessagesUrls.GET_CONVOS_MEMBERS)
suspend fun getConvoMembers(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<MessagesGetConversationMembersResponse>, RestApiError>
): ApiResult<ApiResponse<MessagesGetConvoMembersResponse>, RestApiError>
@FormUrlEncoded
@POST(MessagesUrls.REMOVE_CHAT_USER)
@@ -18,7 +18,7 @@ object MessagesUrls {
const val GET_BY_ID = "$URL/messages.getById"
const val MARK_AS_READ = "$URL/messages.markAsRead"
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 GET_HISTORY_ATTACHMENTS = "$URL/messages.getHistoryAttachments"
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 {
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.UiText
import dev.meloda.fast.ui.R
sealed class ConversationOption(
sealed class ConvoOption(
val title: UiText,
val icon: UiImage
) {
data object MarkAsRead : ConversationOption(
data object MarkAsRead : ConvoOption(
title = UiText.Resource(R.string.action_mark_as_read),
icon = UiImage.Resource(R.drawable.round_done_all_24)
)
data object Pin : ConversationOption(
data object Pin : ConvoOption(
title = UiText.Resource(R.string.action_pin),
icon = UiImage.Resource(R.drawable.pin_outline_24)
)
data object Unpin : ConversationOption(
data object Unpin : ConvoOption(
title = UiText.Resource(R.string.action_unpin),
icon = UiImage.Resource(R.drawable.pin_off_outline_24)
)
data object Delete : ConversationOption(
data object Delete : ConvoOption(
title = UiText.Resource(R.string.action_delete),
icon = UiImage.Resource(R.drawable.round_delete_outline_24)
)
data object Archive : ConversationOption(
title = UiText.Resource(R.string.conversation_context_action_archive),
data object Archive : ConvoOption(
title = UiText.Resource(R.string.convo_context_action_archive),
icon = UiImage.Resource(R.drawable.outline_archive_24)
)
data object Unarchive : ConversationOption(
title = UiText.Resource(R.string.conversation_context_action_unarchive),
data object Unarchive : ConvoOption(
title = UiText.Resource(R.string.convo_context_action_unarchive),
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
)
@@ -0,0 +1,49 @@
package dev.meloda.fast.ui.model.vk
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.AnnotatedString
import dev.meloda.fast.common.model.UiImage
import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.ui.util.ImmutableList
@Stable
sealed class MessageUiItem(
open val id: Long,
open val cmId: Long
) {
@Stable
data class Message(
override val id: Long,
override val cmId: Long,
val text: AnnotatedString?,
val isOut: Boolean,
val fromId: Long,
val date: String,
val randomId: Long,
val isInChat: Boolean,
val name: String,
val showDate: Boolean,
val showAvatar: Boolean,
val showName: Boolean,
val avatar: UiImage,
val isEdited: Boolean,
val isRead: Boolean,
val sendingStatus: SendingStatus,
val isSelected: Boolean,
val isPinned: Boolean,
val isImportant: Boolean,
val attachments: ImmutableList<VkAttachment>?,
val replyCmId: Long?,
val replyTitle: String?,
val replySummary: AnnotatedString?
) : MessageUiItem(id, cmId)
@Stable
data class ActionMessage(
override val id: Long,
override val cmId: Long,
val text: AnnotatedString,
val actionCmId: Long?
) : 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.ui.text.AnnotatedString
@@ -8,7 +8,7 @@ import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.ui.util.ImmutableList
@Immutable
data class UiConversation(
data class UiConvo(
val id: Long,
val lastMessageId: Long?,
val avatar: UiImage?,
@@ -28,5 +28,5 @@ data class UiConversation(
val interactionText: String?,
val isExpanded: 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 dev.meloda.fast.common.model.UiImage
@@ -11,6 +11,10 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.toArgb
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)
}
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="action_mark_as_read">Прочитать</string>
<string name="action_delete">Удалить</string>
<string name="conversation_context_action_unarchive">Из архива</string>
<string name="conversation_context_action_delete">Удалить</string>
<string name="confirm_delete_conversation">Удалить чат?</string>
<string name="convo_context_action_unarchive">Из архива</string>
<string name="convo_context_action_delete">Удалить</string>
<string name="confirm_delete_convo">Удалить чат?</string>
<string name="action_sign_out">Выйти</string>
<string name="sign_out_confirm_title">Выйти?</string>
<string name="conversation_context_action_unpin">Открепить</string>
<string name="conversation_context_action_pin">Закрепить</string>
<string name="confirm_unpin_conversation">Открепить чат?</string>
<string name="confirm_pin_conversation">Закрепить чат?</string>
<string name="confirm_unarchive_conversation">Разархивировать чат?</string>
<string name="convo_context_action_unpin">Открепить</string>
<string name="convo_context_action_pin">Закрепить</string>
<string name="confirm_unpin_convo">Открепить чат?</string>
<string name="confirm_pin_convo">Закрепить чат?</string>
<string name="confirm_unarchive_convo">Разархивировать чат?</string>
<string name="action_pin">Закрепить</string>
<string name="action_unpin">Открепить</string>
<string name="action_mark">Пометить</string>
@@ -181,7 +181,7 @@
<string name="settings_amoled_dark_theme_description">Использовать AMOLED тему с чистым чёрным фоновым цветом, когда используется тёмная тема</string>
<string name="members_count">Участники: %1$d</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_friends">Друзья</string>
<string name="title_profile">Профиль</string>
@@ -262,8 +262,8 @@
<string name="unspam_message_text">Вы уверены, что хотите убрать пометку спама у этого сообщения?</string>
<string name="pin_message_title">Закрепить сообщение</string>
<string name="copied_to_clipboard">Скопировано в буфер обмена</string>
<string name="conversation_context_action_archive">В архив</string>
<string name="confirm_archive_conversation">Архивировать чат?</string>
<string name="convo_context_action_archive">В архив</string>
<string name="confirm_archive_convo">Архивировать чат?</string>
<string name="action_archive">В архив</string>
<string name="autofill">Автозаполнение</string>
<string name="bold">Жирный</string>
+11 -11
View File
@@ -153,18 +153,18 @@
<string name="action_mark_as_read">Read</string>
<string name="action_delete">Delete</string>
<string name="conversation_context_action_archive">Archive</string>
<string name="conversation_context_action_unarchive">Unarchive</string>
<string name="conversation_context_action_delete">Delete</string>
<string name="confirm_delete_conversation">Delete the conversation?</string>
<string name="convo_context_action_archive">Archive</string>
<string name="convo_context_action_unarchive">Unarchive</string>
<string name="convo_context_action_delete">Delete</string>
<string name="confirm_delete_convo">Delete the conversation?</string>
<string name="action_sign_out">Sign out</string>
<string name="sign_out_confirm_title">Sign out?</string>
<string name="conversation_context_action_unpin">Unpin</string>
<string name="conversation_context_action_pin">Pin</string>
<string name="confirm_unpin_conversation">Unpin the conversation?</string>
<string name="confirm_pin_conversation">Pin the conversation?</string>
<string name="confirm_archive_conversation">Archive the conversation?</string>
<string name="confirm_unarchive_conversation">Unarchive the conversation?</string>
<string name="convo_context_action_unpin">Unpin</string>
<string name="convo_context_action_pin">Pin</string>
<string name="confirm_unpin_convo">Unpin the conversation?</string>
<string name="confirm_pin_convo">Pin the conversation?</string>
<string name="confirm_archive_convo">Archive the conversation?</string>
<string name="confirm_unarchive_convo">Unarchive the conversation?</string>
<string name="action_pin">Pin</string>
<string name="action_unpin">Unpin</string>
<string name="action_mark">Mark</string>
@@ -245,7 +245,7 @@
<string name="action_refresh">Refresh</string>
<string name="members_count">Members: %1$d</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_friends">Friends</string>
<string name="title_profile">Profile</string>