chat theme change event

mark messages as important
refactoring
This commit is contained in:
2021-09-14 23:33:10 +03:00
parent 088c6c8712
commit a3a282c32c
55 changed files with 722 additions and 111 deletions
@@ -1,7 +1,10 @@
package com.meloda.fast.api
import android.content.Context
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.text.SpannableString
import android.text.style.StyleSpan
import androidx.core.content.ContextCompat
import com.meloda.fast.R
import com.meloda.fast.api.model.VkGroup
@@ -51,9 +54,7 @@ object VkUtils {
when (baseAttachment.getPreparedType()) {
BaseVkAttachmentItem.AttachmentType.PHOTO -> {
val photo = baseAttachment.photo ?: continue
attachments += VkPhoto(
link = photo.sizes[0].url
)
attachments += photo.asVkPhoto()
}
BaseVkAttachmentItem.AttachmentType.VIDEO -> {
val video = baseAttachment.video ?: continue
@@ -206,8 +207,6 @@ object VkUtils {
val actionUser = profiles?.get(memberId)
val actionGroup = groups?.get(memberId)
// val actionUser = profiles?.find { it.id == memberId }
// val actionGroup = groups?.find { it.id == memberId }
if (isUser && actionUser == null) return null
if (isGroup && actionGroup == null) return null
@@ -233,8 +232,6 @@ object VkUtils {
val actionUser = profiles?.get(memberId)
val actionGroup = groups?.get(memberId)
// val actionUser = profiles?.find { it.id == memberId }
// val actionGroup = groups?.find { it.id == memberId }
if (isUser && actionUser == null) return null
if (isGroup && actionGroup == null) return null
@@ -302,11 +299,257 @@ object VkUtils {
"$prefix took a screenshot"
}
VkMessage.Action.CHAT_STYLE_UPDATE -> {
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
"$prefix changed chat theme"
}
null -> null
else -> "[${message.action}]"
}
}
fun getActionMessageText(
message: VkMessage,
youPrefix: String,
profiles: HashMap<Int, VkUser>? = null,
groups: HashMap<Int, VkGroup>? = null,
messageUser: VkUser? = null,
messageGroup: VkGroup? = null
): SpannableString? {
return when (message.getPreparedAction()) {
VkMessage.Action.CHAT_CREATE -> {
val text = message.actionText ?: return null
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isGroup() -> messageGroup?.name
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
val spanText = "$prefix created «$text»"
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
it.setSpan(
StyleSpan(Typeface.BOLD),
spanText.indexOf(text, startIndex = prefix.length),
text.length, 0
)
}
}
VkMessage.Action.CHAT_TITLE_UPDATE -> {
val text = message.actionText ?: return null
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isGroup() -> messageGroup?.name
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
val spanText = "$prefix renamed chat to «$text»"
val startIndex = spanText.indexOf(text)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
it.setSpan(
StyleSpan(Typeface.BOLD), startIndex, startIndex + text.length, 0
)
}
}
VkMessage.Action.CHAT_PHOTO_UPDATE -> {
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isGroup() -> messageGroup?.name
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
val spanText = "$prefix updated the chat photo"
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
}
VkMessage.Action.CHAT_PHOTO_REMOVE -> {
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isGroup() -> messageGroup?.name
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
val spanText = "$prefix deleted the chat photo"
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
}
VkMessage.Action.CHAT_KICK_USER -> {
val memberId = message.actionMemberId ?: return null
val isUser = memberId > 0
val isGroup = memberId < 0
val actionUser = profiles?.get(memberId)
val actionGroup = groups?.get(memberId)
if (isUser && actionUser == null) return null
if (isGroup && actionGroup == null) return null
if (memberId == message.fromId) {
val prefix = if (memberId == UserConfig.userId) youPrefix
else actionUser.toString()
val spanText = "$prefix left the chat"
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
} else {
val prefix =
if (message.fromId == UserConfig.userId) youPrefix
else messageUser?.toString() ?: messageGroup?.toString() ?: "..."
val postfix =
if (memberId == UserConfig.userId) youPrefix.lowercase()
else actionUser.toString()
val spanText = "$prefix kicked $postfix"
val startIndex = spanText.indexOf(postfix)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
it.setSpan(
StyleSpan(Typeface.BOLD), startIndex, startIndex + postfix.length, 0
)
}
}
}
VkMessage.Action.CHAT_INVITE_USER -> {
val memberId = message.actionMemberId ?: 0
val isUser = memberId > 0
val isGroup = memberId < 0
val actionUser = profiles?.get(memberId)
val actionGroup = groups?.get(memberId)
if (isUser && actionUser == null) return null
if (isGroup && actionGroup == null) return null
if (memberId == message.fromId) {
val prefix = if (memberId == UserConfig.userId) youPrefix
else actionUser.toString()
val spanText = "$prefix returned the chat"
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
} else {
val prefix = if (message.fromId == UserConfig.userId) youPrefix
else messageUser?.toString() ?: messageGroup?.toString() ?: "..."
val postfix =
if (memberId == UserConfig.userId) youPrefix.lowercase()
else actionUser.toString()
val spanText = "$prefix invited $postfix"
val startIndex = spanText.indexOf(postfix)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
it.setSpan(
StyleSpan(Typeface.BOLD), startIndex, startIndex + postfix.length, 0
)
}
}
}
VkMessage.Action.CHAT_INVITE_USER_BY_LINK -> {
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
val spanText = "$prefix joined the chat via link"
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
}
VkMessage.Action.CHAT_INVITE_USER_BY_CALL_LINK -> {
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
val spanText = "$prefix joined the call via link"
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
}
VkMessage.Action.CHAT_PIN_MESSAGE -> {
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isGroup() -> messageGroup?.name
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
val actionMessage = message.actionMessage ?: return null
val spanText = "$prefix pinned message «$actionMessage»"
val startIndex = spanText.indexOf(actionMessage)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
it.setSpan(
StyleSpan(Typeface.BOLD), startIndex, startIndex + actionMessage.length, 0
)
}
}
VkMessage.Action.CHAT_UNPIN_MESSAGE -> {
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isGroup() -> messageGroup?.name
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
val spanText = "$prefix unpinned message"
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
}
VkMessage.Action.CHAT_SCREENSHOT -> {
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isGroup() -> messageGroup?.name
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
val spanText = "$prefix took a screenshot"
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
}
VkMessage.Action.CHAT_STYLE_UPDATE -> {
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
val spanText = "$prefix changed chat theme"
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
}
null -> null
else -> SpannableString("[${message.action}]")
}
}
fun getForwardsConversationText(context: Context, message: VkMessage): String? {
if (message.forwards.isNullOrEmpty()) return null
@@ -1,18 +0,0 @@
package com.meloda.fast.api.datasource
import com.meloda.fast.api.network.repo.MessagesRepo
import com.meloda.fast.api.network.request.MessagesGetHistoryRequest
import com.meloda.fast.api.network.request.MessagesSendRequest
import com.meloda.fast.database.dao.MessagesDao
import javax.inject.Inject
class MessagesDataSource @Inject constructor(
private val repo: MessagesRepo,
private val dao: MessagesDao
) {
suspend fun getHistory(params: MessagesGetHistoryRequest) = repo.getHistory(params.map)
suspend fun send(params: MessagesSendRequest) = repo.send(params.map)
}
@@ -24,7 +24,8 @@ data class VkMessage(
val actionText: String? = null,
val actionConversationMessageId: Int? = null,
val actionMessage: String? = null,
val geoType: String? = null
val geoType: String? = null,
val important: Boolean = false
) : Parcelable {
@IgnoredOnParcel
@@ -61,7 +62,8 @@ data class VkMessage(
actionText = actionText,
actionConversationMessageId = actionConversationMessageId,
actionMessage = actionMessage,
geoType = geoType
geoType = geoType,
important = important
)
enum class Action(val value: String) {
@@ -78,7 +80,8 @@ data class VkMessage(
// TODO: 9/11/2021 catch this shit
CHAT_INVITE_USER_BY_CALL("chat_invite_user_by_call"),
CHAT_INVITE_USER_BY_CALL_LINK("chat_invite_user_by_call_join_link");
CHAT_INVITE_USER_BY_CALL_LINK("chat_invite_user_by_call_join_link"),
CHAT_STYLE_UPDATE("conversation_style_update");
companion object {
fun parse(value: String) = values().first { it.value == value }
@@ -1,3 +1,5 @@
package com.meloda.fast.api.model.attachments
abstract class VkAttachment
import android.os.Parcelable
abstract class VkAttachment : Parcelable
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkAudio(
val link: String
) : VkAttachment()
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkCall(
val initiatorId: Int
) : VkAttachment()
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkFile(
val link: String
) : VkAttachment()
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkGift(
val link: String
) : VkAttachment()
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkGraffiti(
val link: String
) : VkAttachment()
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkGroupCall(
val initiatorId: Int
) : VkAttachment()
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkLink(
val link: String
) : VkAttachment()
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkMiniApp(
val link: String
) : VkAttachment()
@@ -1,5 +1,28 @@
package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.Size
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkPhoto(
val link: String
) : VkAttachment()
val albumId: Int,
val date: Int,
val id: Int,
val ownerId: Int,
val hasTags: Boolean,
val accessKey: String?,
val sizes: List<Size>,
val text: String,
val userId: Int?
) : VkAttachment() {
fun sizeOfType(type: Char): Size? {
for (size in sizes) {
if (size.type == type.toString())
return size
}
return null
}
}
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkPoll(
val id: Int
) : VkAttachment()
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkSticker(
val link: String
) : VkAttachment()
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkVideo(
val link: String
) : VkAttachment()
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkVoiceMessage(
val link: String
) : VkAttachment()
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkWall(
val id: Int
) : VkAttachment()
@@ -1,5 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkWallReply(
val id: Int
) : VkAttachment()
@@ -46,7 +46,8 @@ data class BaseVkMessage(
actionText = action?.text,
actionConversationMessageId = action?.conversationMessageId,
actionMessage = action?.message,
geoType = geo?.type
geoType = geo?.type,
important = important
).also {
it.attachments = VkUtils.parseAttachments(attachments)
it.forwards = VkUtils.parseForwards(fwdMessages)
@@ -2,6 +2,7 @@ package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.attachments.VkPhoto
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -15,12 +16,26 @@ data class BaseVkPhoto(
@SerializedName("has_tags")
val hasTags: Boolean,
@SerializedName("access_key")
val accessKey: String,
val accessKey: String?,
val sizes: List<Size>,
val text: String,
@SerializedName("user_id")
val userId: Int?
) : BaseVkAttachment()
) : BaseVkAttachment() {
fun asVkPhoto() = VkPhoto(
albumId = albumId,
date = date,
id = id,
ownerId = ownerId,
hasTags = hasTags,
accessKey = accessKey,
sizes = sizes,
text = text,
userId = userId
)
}
@Parcelize
data class Size(
@@ -1,4 +1,4 @@
package com.meloda.fast.api.network.request
package com.meloda.fast.api.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
@@ -1,4 +1,4 @@
package com.meloda.fast.api.network.request
package com.meloda.fast.api.model.request
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@@ -1,6 +1,7 @@
package com.meloda.fast.api.network.request
package com.meloda.fast.api.model.request
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -54,5 +55,19 @@ data class MessagesSendRequest(
disableMentions?.let { this["disable_mentions"] = (if (it) 1 else 0).toString() }
dontParseLinks?.let { this["dont_parse_links"] = (if (it) 1 else 0).toString() }
}
}
@Parcelize
data class MessagesMarkAsImportantRequest(
@SerializedName("message_ids")
val messagesIds: List<Int>,
val important: Boolean
) : Parcelable {
val map
get() = mutableMapOf(
"message_ids" to messagesIds.joinToString { it.toString() },
"important" to (if (important) 1 else 0).toString()
)
}
@@ -1,4 +1,4 @@
package com.meloda.fast.api.network.request
package com.meloda.fast.api.model.request
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@@ -1,4 +1,4 @@
package com.meloda.fast.api.network.response
package com.meloda.fast.api.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
@@ -1,4 +1,4 @@
package com.meloda.fast.api.network.response
package com.meloda.fast.api.model.response
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
@@ -1,4 +1,4 @@
package com.meloda.fast.api.network.response
package com.meloda.fast.api.model.response
import android.os.Parcelable
import com.meloda.fast.api.model.base.BaseVkConversation
@@ -0,0 +1,2 @@
package com.meloda.fast.api.model.response
@@ -21,6 +21,7 @@ object VKUrls {
object Messages {
const val GetHistory = "$API/messages.getHistory"
const val Send = "$API/messages.send"
const val MarkAsImportant = "$API/messages.markAsImportant"
}
@@ -1,7 +1,7 @@
package com.meloda.fast.api.datasource
package com.meloda.fast.api.network.datasource
import com.meloda.fast.api.network.repo.AuthRepo
import com.meloda.fast.api.network.request.RequestAuthDirect
import com.meloda.fast.api.model.request.RequestAuthDirect
import javax.inject.Inject
class AuthDataSource @Inject constructor(
@@ -1,8 +1,8 @@
package com.meloda.fast.api.datasource
package com.meloda.fast.api.network.datasource
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.network.repo.ConversationsRepo
import com.meloda.fast.api.network.request.ConversationsGetRequest
import com.meloda.fast.api.model.request.ConversationsGetRequest
import com.meloda.fast.database.dao.ConversationsDao
import javax.inject.Inject
@@ -0,0 +1,24 @@
package com.meloda.fast.api.network.datasource
import com.meloda.fast.api.model.request.MessagesGetHistoryRequest
import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest
import com.meloda.fast.api.model.request.MessagesSendRequest
import com.meloda.fast.api.network.repo.MessagesRepo
import com.meloda.fast.database.dao.MessagesDao
import javax.inject.Inject
class MessagesDataSource @Inject constructor(
private val repo: MessagesRepo,
private val dao: MessagesDao
) {
suspend fun getHistory(params: MessagesGetHistoryRequest) =
repo.getHistory(params.map)
suspend fun send(params: MessagesSendRequest) =
repo.send(params.map)
suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) =
repo.markAsImportant(params.map)
}
@@ -1,8 +1,8 @@
package com.meloda.fast.api.datasource
package com.meloda.fast.api.network.datasource
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.network.repo.UsersRepo
import com.meloda.fast.api.network.request.UsersGetRequest
import com.meloda.fast.api.model.request.UsersGetRequest
import com.meloda.fast.database.dao.UsersDao
import javax.inject.Inject
@@ -1,9 +1,9 @@
package com.meloda.fast.api.network.repo
import com.meloda.fast.api.network.VKUrls
import com.meloda.fast.api.network.response.ResponseAuthDirect
import com.meloda.fast.api.model.response.ResponseAuthDirect
import com.meloda.fast.api.network.Answer
import com.meloda.fast.api.network.response.ResponseSendSms
import com.meloda.fast.api.model.response.ResponseSendSms
import retrofit2.http.*
interface AuthRepo {
@@ -3,7 +3,7 @@ package com.meloda.fast.api.network.repo
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.network.Answer
import com.meloda.fast.api.network.VKUrls
import com.meloda.fast.api.network.response.ConversationsGetResponse
import com.meloda.fast.api.model.response.ConversationsGetResponse
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
@@ -1,9 +1,9 @@
package com.meloda.fast.api.network.repo
import com.meloda.fast.api.base.ApiResponse
import com.meloda.fast.api.model.response.MessagesGetHistoryResponse
import com.meloda.fast.api.network.Answer
import com.meloda.fast.api.network.VKUrls
import com.meloda.fast.api.network.response.MessagesGetHistoryResponse
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
@@ -18,4 +18,8 @@ interface MessagesRepo {
@POST(VKUrls.Messages.Send)
suspend fun send(@FieldMap params: Map<String, String>): Answer<ApiResponse<Int>>
@FormUrlEncoded
@POST(VKUrls.Messages.MarkAsImportant)
suspend fun markAsImportant(@FieldMap params: Map<String, String>): Answer<ApiResponse<List<Int>>>
}
@@ -1,5 +0,0 @@
package com.meloda.fast.api.network.response
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@@ -18,7 +18,7 @@ import com.meloda.fast.database.dao.UsersDao
VkUser::class,
VkGroup::class
],
version = 15,
version = 16,
exportSchema = false
)
abstract class AppDatabase : RoomDatabase() {
@@ -2,10 +2,10 @@ package com.meloda.fast.di
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.meloda.fast.api.datasource.AuthDataSource
import com.meloda.fast.api.datasource.ConversationsDataSource
import com.meloda.fast.api.datasource.MessagesDataSource
import com.meloda.fast.api.datasource.UsersDataSource
import com.meloda.fast.api.network.datasource.AuthDataSource
import com.meloda.fast.api.network.datasource.ConversationsDataSource
import com.meloda.fast.api.network.datasource.MessagesDataSource
import com.meloda.fast.api.network.datasource.UsersDataSource
import com.meloda.fast.api.network.AuthInterceptor
import com.meloda.fast.api.network.ResultCallFactory
import com.meloda.fast.api.network.repo.AuthRepo
@@ -9,7 +9,6 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import coil.load
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar
import com.meloda.fast.R
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.model.VkConversation
@@ -27,34 +26,38 @@ class ConversationsFragment :
BaseViewModelFragment<ConversationsViewModel>(R.layout.fragment_conversations) {
companion object {
val TAG: String = ConversationsFragment::class.java.name
const val TAG = "ConversationsFragment"
}
override val viewModel: ConversationsViewModel by viewModels()
private val binding: FragmentConversationsBinding by viewBinding()
private lateinit var adapter: ConversationsAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.loadProfileUser()
prepareViews()
adapter = ConversationsAdapter(requireContext(), mutableListOf()).also {
private val adapter: ConversationsAdapter by lazy {
ConversationsAdapter(
requireContext(),
mutableListOf(),
hashMapOf(),
hashMapOf()
).also {
it.itemClickListener = this::onItemClick
it.itemLongClickListener = this::onItemLongClick
}
}
private var isPaused = false
override fun onPause() {
super.onPause()
isPaused = true
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
prepareViews()
binding.recyclerView.adapter = adapter
viewModel.loadConversations()
binding.createChat.setOnClickListener {
Snackbar.make(it, "Test Snackbar with action", Snackbar.LENGTH_LONG)
.setAction("Action") {}.show()
}
binding.createChat.setOnClickListener {}
UserConfig.vkUser.observe(viewLifecycleOwner) {
it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } }
@@ -68,10 +71,16 @@ class ConversationsFragment :
val alpha = 1 - abs(verticalOffset * 0.01).toFloat()
// println("offset: $verticalOffset; alpha: $alpha")
binding.avatarContainer.alpha = alpha
})
if (isPaused) {
isPaused = false
return
}
viewModel.loadProfileUser()
viewModel.loadConversations()
}
override fun onEvent(event: VKEvent) {
@@ -3,13 +3,13 @@ package com.meloda.fast.screens.conversations
import androidx.lifecycle.viewModelScope
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.datasource.ConversationsDataSource
import com.meloda.fast.api.datasource.UsersDataSource
import com.meloda.fast.api.network.datasource.ConversationsDataSource
import com.meloda.fast.api.network.datasource.UsersDataSource
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.network.request.ConversationsGetRequest
import com.meloda.fast.api.network.request.UsersGetRequest
import com.meloda.fast.api.model.request.ConversationsGetRequest
import com.meloda.fast.api.model.request.UsersGetRequest
import com.meloda.fast.base.viewmodel.BaseViewModel
import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent
@@ -6,8 +6,8 @@ import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.VKException
import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.datasource.AuthDataSource
import com.meloda.fast.api.network.request.RequestAuthDirect
import com.meloda.fast.api.network.datasource.AuthDataSource
import com.meloda.fast.api.model.request.RequestAuthDirect
import com.meloda.fast.base.viewmodel.BaseViewModel
import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent
@@ -3,19 +3,22 @@ package com.meloda.fast.screens.messages
import android.content.Context
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import coil.load
import com.meloda.fast.R
import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.attachments.VkPhoto
import com.meloda.fast.base.adapter.BaseAdapter
import com.meloda.fast.base.adapter.BaseHolder
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.databinding.ItemMessageInBinding
import com.meloda.fast.databinding.ItemMessageOutBinding
import com.meloda.fast.databinding.ItemMessageServiceBinding
import com.meloda.fast.databinding.*
import com.meloda.fast.util.AndroidUtils
import kotlin.math.roundToInt
@@ -37,6 +40,15 @@ class MessagesHistoryAdapter constructor(
if (viewType == -1) {
getItem(position).let {
if (it.action != null) viewType = SERVICE
val attachments = it.attachments ?: return@let
if (attachments.isEmpty()) return@let
if (VkUtils.isAttachmentsHaveOneType(attachments) &&
attachments[0] is VkPhoto
) {
return if (it.isOut) ATTACHMENT_PHOTOS_OUT else ATTACHMENT_PHOTOS_IN
}
if (it.isOut) viewType = OUTGOING
if (!it.isOut) viewType = INCOMING
}
@@ -52,9 +64,21 @@ class MessagesHistoryAdapter constructor(
return when (viewType) {
HEADER -> Header(createEmptyView(60))
FOOTER -> Footer(createEmptyView(36))
SERVICE -> ServiceMessage(ItemMessageServiceBinding.inflate(inflater, parent, false))
OUTGOING -> OutgoingMessage(ItemMessageOutBinding.inflate(inflater, parent, false))
INCOMING -> IncomingMessage(ItemMessageInBinding.inflate(inflater, parent, false))
SERVICE -> ServiceMessage(
ItemMessageServiceBinding.inflate(inflater, parent, false)
)
ATTACHMENT_PHOTOS_IN -> AttachmentPhotosIncoming(
ItemMessageAttachmentPhotoInBinding.inflate(inflater, parent, false)
)
ATTACHMENT_PHOTOS_OUT -> AttachmentPhotosOutgoing(
ItemMessageAttachmentPhotoOutBinding.inflate(inflater, parent, false)
)
OUTGOING -> OutgoingMessage(
ItemMessageOutBinding.inflate(inflater, parent, false)
)
INCOMING -> IncomingMessage(
ItemMessageInBinding.inflate(inflater, parent, false)
)
else -> Holder()
}
}
@@ -83,12 +107,87 @@ class MessagesHistoryAdapter constructor(
inner class Footer(v: View) : Holder(v)
inner class AttachmentPhotosIncoming(
private val binding: ItemMessageAttachmentPhotoInBinding
) : Holder(binding.root) {
init {
binding.photo.shapeAppearanceModel = binding.photo.shapeAppearanceModel.withCornerSize {
AndroidUtils.px(12)
}
}
override fun bind(position: Int) {
val message = getItem(position)
val photo = message.attachments?.get(0) as? VkPhoto ?: return
val size = photo.sizeOfType('m') ?: return
binding.photo.layoutParams = FrameLayout.LayoutParams(
AndroidUtils.px(size.width).roundToInt(),
AndroidUtils.px(size.height).roundToInt()
)
binding.photo.load(size.url)
}
}
inner class AttachmentPhotosOutgoing(
private val binding: ItemMessageAttachmentPhotoOutBinding
) : Holder(binding.root) {
init {
binding.photo.shapeAppearanceModel = binding.photo.shapeAppearanceModel.withCornerSize {
AndroidUtils.px(12)
}
}
override fun bind(position: Int) {
val message = getItem(position)
val photo = message.attachments?.get(0) as? VkPhoto ?: return
val size = photo.sizeOfType('m') ?: return
binding.photo.layoutParams = LinearLayoutCompat.LayoutParams(
AndroidUtils.px(size.width).roundToInt(),
AndroidUtils.px(size.height).roundToInt()
)
binding.photo.load(size.url)
}
}
inner class ServiceMessage(
private val binding: ItemMessageServiceBinding
) : Holder(binding.root) {
override fun bind(position: Int) {
private val youPrefix = context.getString(R.string.you_message_prefix)
override fun bind(position: Int) {
val message = getItem(position)
val messageUser =
if (message.isUser()) profiles[message.fromId]
else null
val messageGroup =
if (message.isGroup()) groups[message.fromId]
else null
message.action ?: return
binding.message.text = VkUtils.getActionMessageText(
message = message,
youPrefix = youPrefix,
profiles = profiles,
groups = groups,
messageUser = messageUser,
messageGroup = messageGroup
)
}
}
@@ -176,11 +275,14 @@ class MessagesHistoryAdapter constructor(
}
companion object {
private const val INCOMING = 1001
private const val OUTGOING = 1002
private const val SERVICE = 1003
private const val SERVICE = 1
private const val HEADER = 0
private const val FOOTER = 2
private const val INCOMING = 3
private const val OUTGOING = 4
private const val ATTACHMENT_PHOTOS_IN = 101
private const val ATTACHMENT_PHOTOS_OUT = 1011
private val COMPARATOR = object : DiffUtil.ItemCallback<VkMessage>() {
override fun areItemsTheSame(
@@ -12,6 +12,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import coil.load
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.meloda.fast.R
import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.model.VkConversation
@@ -57,7 +58,10 @@ class MessagesHistoryFragment :
}
private val adapter: MessagesHistoryAdapter by lazy {
MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation)
MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also {
it.itemClickListener = this::onItemClick
it.itemLongClickListener = this::onItemLongClick
}
}
private var timestampTimer: Timer? = null
@@ -183,6 +187,7 @@ class MessagesHistoryFragment :
}
}
private fun performAction() {
if (action.value == Action.RECORD) {
@@ -203,7 +208,7 @@ class MessagesHistoryFragment :
)
adapter.add(message)
adapter.notifyItemRangeInserted(adapter.lastPosition - 1, 1)
adapter.notifyDataSetChanged()
binding.recyclerView.smoothScrollToPosition(adapter.lastPosition)
binding.message.clear()
@@ -217,6 +222,7 @@ class MessagesHistoryFragment :
override fun onEvent(event: VKEvent) {
when (event) {
is MessagesMarkAsImportant -> markMessagesAsImportant(event)
is MessagesLoaded -> refreshMessages(event)
is StartProgressEvent -> onProgressStarted()
is StopProgressEvent -> onProgressStopped()
@@ -264,6 +270,19 @@ class MessagesHistoryFragment :
}
}
private fun markMessagesAsImportant(event: MessagesMarkAsImportant) {
var changed = false
for (i in adapter.values.indices) {
val message = adapter.values[i]
if (event.messagesIds.contains(message.id)) {
if (!changed) changed = true
adapter.values[i] = message.copy(important = event.important)
}
}
if (changed) adapter.notifyDataSetChanged()
}
private fun refreshMessages(event: MessagesLoaded) {
adapter.profiles += event.profiles
adapter.groups += event.groups
@@ -282,4 +301,30 @@ class MessagesHistoryFragment :
else binding.recyclerView.scrollToPosition(adapter.lastPosition)
}
private fun onItemClick(position: Int) {
val message = adapter.values[position]
val important = if (message.important) "Unmark as important" else "Mark as important"
val params = arrayOf(important)
val dialog = MaterialAlertDialogBuilder(requireContext())
.setItems(params) { _, which ->
if (which == 0) {
viewModel.markAsImportant(
messagesIds = listOf(message.id),
important = !message.important
)
}
}
dialog.show()
}
private fun onItemLongClick(position: Int): Boolean {
return true
}
}
@@ -1,13 +1,14 @@
package com.meloda.fast.screens.messages
import androidx.lifecycle.viewModelScope
import com.meloda.fast.api.datasource.MessagesDataSource
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.network.request.MessagesGetHistoryRequest
import com.meloda.fast.api.network.request.MessagesSendRequest
import com.meloda.fast.api.model.request.MessagesGetHistoryRequest
import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest
import com.meloda.fast.api.model.request.MessagesSendRequest
import com.meloda.fast.api.network.datasource.MessagesDataSource
import com.meloda.fast.base.viewmodel.BaseViewModel
import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent
@@ -109,6 +110,33 @@ class MessagesHistoryViewModel @Inject constructor(
})
}
fun markAsImportant(
messagesIds: List<Int>,
important: Boolean
) = viewModelScope.launch {
makeJob({
dataSource.markAsImportant(
MessagesMarkAsImportantRequest(
messagesIds = messagesIds,
important = important
)
)
},
onAnswer = {
val response = it.response ?: return@makeJob
sendEvent(
MessagesMarkAsImportant(
messagesIds = response,
important = important
)
)
},
onError = {
val throwable = it
val i = 0
})
}
}
data class MessagesLoaded(
@@ -117,4 +145,9 @@ data class MessagesLoaded(
val messages: List<VkMessage>,
val profiles: HashMap<Int, VkUser>,
val groups: HashMap<Int, VkGroup>
) : VKEvent()
data class MessagesMarkAsImportant(
val messagesIds: List<Int>,
val important: Boolean
) : VKEvent()
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
tools:src="@tools:sample/backgrounds/scenic" />
</FrameLayout>
</layout>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:padding="12dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:scaleType="centerCrop"
tools:src="@tools:sample/backgrounds/scenic" />
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
@@ -3,15 +3,19 @@
xmlns:tools="http://schemas.android.com/tools">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:gravity="center"
android:orientation="vertical"
android:paddingHorizontal="12dp"
android:paddingVertical="2.5dp">
<TextView
android:id="@+id/message"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Service" />
android:textColor="?textColorService"
tools:text="Service" />
</androidx.appcompat.widget.LinearLayoutCompat>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/conversationsFragment">
<include app:graph="@navigation/messages" />
</navigation>
+15 -2
View File
@@ -10,6 +10,10 @@
@android:color/transparent
</item>
<item name="android:windowLightStatusBar">true</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryVariant">@color/colorPrimaryVariant</item>
<item name="colorOnPrimary">@color/colorOnPrimary</item>
@@ -18,9 +22,18 @@
<item name="colorSecondaryVariant">@color/colorSecondaryVariant</item>
<item name="colorOnSecondary">@color/colorOnSecondary</item>
<item name="android:windowLightStatusBar">true</item>
<item name="colorSecondary2">@color/colorSecondary2</item>
<item name="colorSecondary2Variant">@color/colorSecondary2Variant</item>
<item name="colorOnSecondary2">@color/colorOnSecondary2</item>
<item name="preferenceTheme">@style/PreferenceThemeOverlay</item>
<item name="colorSecondary3">@color/colorSecondary3</item>
<item name="colorSecondary3Variant">@color/colorSecondary3Variant</item>
<item name="colorOnSecondary3">@color/colorOnSecondary3</item>
<item name="textColorPrimary">@color/textColorPrimary</item>
<item name="textColorSecondary">@color/textColorSecondary</item>
<item name="textColorSecondaryVariant">@color/textColorSecondaryVariant</item>
<item name="textColorService">@color/textColorService</item>
<item name="dialogCornerRadius">12dp</item>
</style>
+1
View File
@@ -25,5 +25,6 @@
<color name="n2_10">@android:color/system_neutral2_10</color>
<color name="n2_100">@android:color/system_neutral2_100</color>
<color name="n2_500">@android:color/system_neutral2_500</color>
<color name="n2_600">@android:color/system_neutral2_600</color>
</resources>
+4 -3
View File
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="textColorPrimary" format="color"/>
<attr name="textColorSecondary" format="color"/>
<attr name="textColorSecondaryVariant" format="color"/>
<attr name="textColorPrimary" format="color" />
<attr name="textColorSecondary" format="color" />
<attr name="textColorSecondaryVariant" format="color" />
<attr name="textColorService" format="color" />
<attr name="colorSecondary2" format="color" />
<attr name="colorSecondary2Variant" format="color" />
+2
View File
@@ -22,6 +22,7 @@
<color name="textColorPrimary">@color/n1_900</color>
<color name="textColorSecondary">@color/n1_900</color>
<color name="textColorSecondaryVariant">@color/n2_500</color>
<color name="textColorService">@color/n2_600</color>
<color name="colorSurface">@color/a1_0</color>
@@ -50,5 +51,6 @@
<color name="n2_10">#FDFBFE</color>
<color name="n2_100">#E0E2EB</color>
<color name="n2_500">#74767D</color>
<color name="n2_600">#5C5E65</color>
</resources>
+1
View File
@@ -30,6 +30,7 @@
<item name="textColorPrimary">@color/textColorPrimary</item>
<item name="textColorSecondary">@color/textColorSecondary</item>
<item name="textColorSecondaryVariant">@color/textColorSecondaryVariant</item>
<item name="textColorService">@color/textColorService</item>
<item name="colorSurface">@color/colorSurface</item>