* refactor Conversation -> Convo
* extract Message and Convo mappers to core/domain module * improve reply container text
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
+8
-8
@@ -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>>
|
||||
|
||||
+15
-15
@@ -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
|
||||
+7
-7
@@ -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("&", "&")
|
||||
.replace(""", "\"")
|
||||
.replace("<br>", " ")
|
||||
.replace(">", ">")
|
||||
.replace("<", "<")
|
||||
.replace("<br/>", " ")
|
||||
.replace("–", "-")
|
||||
.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
|
||||
)
|
||||
+1
-1
@@ -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
|
||||
Reference in New Issue
Block a user