code saving

This commit is contained in:
2021-09-21 13:39:34 +03:00
parent d1ed98691c
commit 56fb93d2e4
43 changed files with 665 additions and 122 deletions
@@ -7,7 +7,6 @@ object VKConstants {
const val USER_FIELDS =
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info"
const val API_VERSION = "5.132"
const val VK_APP_ID = "2274003"
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
@@ -27,7 +27,9 @@ object VkUtils {
return throwable.error == VkErrors.NEED_CAPTCHA
}
fun prepareMessageText(text: String): String {
fun prepareMessageText(text: String?): String? {
if (text == null) return null
return text
.replace("\n", " ")
.replace("&amp", "&")
@@ -58,9 +60,7 @@ object VkUtils {
}
BaseVkAttachmentItem.AttachmentType.VIDEO -> {
val video = baseAttachment.video ?: continue
attachments += VkVideo(
link = video.player
)
attachments += video.asVkVideo()
}
BaseVkAttachmentItem.AttachmentType.AUDIO -> {
val audio = baseAttachment.audio ?: continue
@@ -1,8 +1,8 @@
package com.meloda.fast.api.model
import android.os.Parcelable
import androidx.room.Embedded
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
@@ -24,10 +24,14 @@ data class VkConversation(
val lastMessageId: Int,
val unreadCount: Int?,
val membersCount: Int?,
val isPinned: Boolean
) : Parcelable {
@Ignore
val isPinned: Boolean,
@Embedded(prefix = "pinnedMessage_")
var pinnedMessage: VkMessage? = null,
@Embedded(prefix = "lastMessage_")
var lastMessage: VkMessage? = null
) : Parcelable {
fun isChat() = type == "chat"
fun isUser() = type == "user"
@@ -2,10 +2,8 @@ package com.meloda.fast.api.model
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey
import com.meloda.fast.api.model.attachments.VkAttachment
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Entity(tableName = "messages")
@@ -25,16 +23,10 @@ data class VkMessage(
val actionConversationMessageId: Int? = null,
val actionMessage: String? = null,
val geoType: String? = null,
val important: Boolean = false
) : Parcelable {
@IgnoredOnParcel
@Ignore
var forwards: List<VkMessage>? = null
@IgnoredOnParcel
@Ignore
val important: Boolean = false,
var forwards: List<VkMessage>? = null,
var attachments: List<VkAttachment>? = null
) : Parcelable {
fun isPeerChat() = peerId > 2_000_000_000
@@ -1,5 +1,7 @@
package com.meloda.fast.api.model.attachments
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
abstract class VkAttachment : Parcelable
@Parcelize
open class VkAttachment : Parcelable
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkAudio(
val link: String
) : VkAttachment()
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,14 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkCall(
val initiatorId: Int
) : VkAttachment()
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkFile(
val link: String
) : VkAttachment()
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkGift(
val link: String
) : VkAttachment()
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkGraffiti(
val link: String
) : VkAttachment()
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkGroupCall(
val initiatorId: Int
) : VkAttachment()
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkLink(
val link: String
) : VkAttachment()
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkMiniApp(
val link: String
) : VkAttachment()
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,6 +1,7 @@
package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.Size
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -16,6 +17,9 @@ data class VkPhoto(
val userId: Int?
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
fun sizeOfType(type: Char): Size? {
for (size in sizes) {
if (size.type == type.toString())
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkPoll(
val id: Int
) : VkAttachment()
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -2,6 +2,7 @@ package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.BaseVkSticker
import com.meloda.fast.api.model.base.attachments.StickerSize
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -12,6 +13,9 @@ data class VkSticker(
val backgroundImages: List<BaseVkSticker.Image>
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
fun urlForSize(@StickerSize size: Int): String? {
for (image in images) {
if (image.width == size) return image.url
@@ -1,8 +1,21 @@
package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.BaseVkVideo
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkVideo(
val link: String
) : VkAttachment()
val id: Int,
val images: List<BaseVkVideo.Image>,
val firstFrames: List<BaseVkVideo.FirstFrame>
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
fun imageForWidth(width: Int): BaseVkVideo.Image? {
return images.find { it.width == width }
}
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkVoiceMessage(
val link: String
) : VkAttachment()
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkWall(
val id: Int
) : VkAttachment()
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkWallReply(
val id: Int
) : VkAttachment()
) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -54,7 +54,10 @@ data class BaseVkConversation(
membersCount = chatSettings?.membersCount,
ownerId = chatSettings?.ownerId,
isPinned = sortId.majorId > 0
).apply { this.lastMessage = lastMessage }
).apply {
this.lastMessage = lastMessage
this.pinnedMessage = chatSettings?.pinnedMessage?.asVkMessage()
}
@Parcelize
data class Peer(
@@ -111,7 +114,9 @@ data class BaseVkConversation(
val isDisappearing: Boolean,
@SerializedName("is_service")
val isService: Boolean,
val theme: String
val theme: String?,
@SerializedName("pinned_message")
val pinnedMessage: BaseVkMessage?
) : Parcelable {
@Parcelize
@@ -2,8 +2,10 @@ 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.VkVideo
import kotlinx.parcelize.Parcelize
//not all fields
@Parcelize
data class BaseVkVideo(
val id: Int,
@@ -53,6 +55,12 @@ data class BaseVkVideo(
//ads
) : BaseVkAttachment() {
fun asVkVideo() = VkVideo(
id = id,
images = image,
firstFrames = firstFrame
)
@Parcelize
data class Image(
val height: Int,
@@ -1,9 +1,11 @@
package com.meloda.fast.api.network
import com.meloda.fast.api.VKException
import com.meloda.fast.api.base.ApiResponse
import okhttp3.Request
import okio.IOException
import okio.Timeout
import org.json.JSONObject
import retrofit2.*
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
@@ -86,6 +88,9 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
}
} else Answer.Error(IOException(response.errorBody()?.string() ?: ""))
if (result is Answer.Error) if (checkErrors(call, result)) return
callback.onResponse(proxy, Response.success(result))
}
@@ -95,6 +100,23 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
Response.success(Answer.Error(throwable = error))
)
}
private fun checkErrors(call: Call<T>, result: Answer.Error): Boolean {
val json = JSONObject(result.throwable.message ?: "{}")
return if (json.has("error")) {
val error = json.optString("error", "")
val description = json.optString("error_description", "")
val exception = VKException(
error = error,
description = description,
).also { it.json = json }
onFailure(call, exception)
true
} else false
}
}
override fun timeout(): Timeout {
@@ -1,5 +1,6 @@
package com.meloda.fast.api.network.datasource
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.request.MessagesGetHistoryRequest
import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest
import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest
@@ -25,4 +26,8 @@ class MessagesDataSource @Inject constructor(
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
repo.getLongPollServer(params.map)
suspend fun storeMessages(messages: List<VkMessage>) = dao.insert(messages)
suspend fun getCachedMessages(peerId: Int) = dao.getByPeerId(peerId)
}
@@ -1,6 +1,5 @@
package com.meloda.fast.base.viewmodel
import android.util.Log
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.meloda.fast.api.VKException
@@ -14,6 +13,8 @@ import kotlinx.coroutines.launch
abstract class BaseViewModel : ViewModel() {
var unknownErrorDefaultText: String = ""
protected val tasksEventChannel = Channel<VKEvent>()
val tasksEvent = tasksEventChannel.receiveAsFlow()
@@ -30,6 +31,12 @@ abstract class BaseViewModel : ViewModel() {
is Answer.Error -> {
checkErrors(response.throwable)
onError?.invoke(response.throwable)
?: sendEvent(
ErrorEvent(
response.throwable.message
?: unknownErrorDefaultText
)
)
}
}
}.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } }
@@ -37,31 +44,32 @@ abstract class BaseViewModel : ViewModel() {
protected suspend fun <T : VKEvent> sendEvent(event: T) = tasksEventChannel.send(event)
private suspend fun checkErrors(throwable: Throwable) {
if (throwable is ApiError) {
when (throwable.errorCode) {
VkErrorCodes.USER_AUTHORIZATION_FAILED -> {
sendEvent(IllegalTokenEvent)
return
when (throwable) {
is ApiError -> {
when (throwable.errorCode) {
VkErrorCodes.USER_AUTHORIZATION_FAILED -> {
sendEvent(IllegalTokenEvent)
}
}
}
} else if (throwable is VKException) {
when (throwable.error) {
VkErrors.NEED_CAPTCHA -> {
throwable.captcha =
(throwable.json?.optString("captcha_sid")
?: "") to (throwable.json?.optString("captcha_img") ?: "")
return
is VKException -> {
when (throwable.error) {
VkErrors.NEED_CAPTCHA -> {
val json = throwable.json ?: return
sendEvent(
CaptchaEvent(
sid = json.optString("captcha_sid"),
image = json.optString("captcha_img")
)
)
}
VkErrors.NEED_VALIDATION -> {
val json = throwable.json ?: return
sendEvent(ValidationEvent(sid = json.optString("validation_sid")))
}
}
VkErrors.NEED_VALIDATION -> {
throwable.validationSid = throwable.json?.optString("validation_sid")
return
}
}
}
sendEvent(ShowDialogInfoEvent(null, Log.getStackTraceString(throwable)))
}
}
@@ -10,6 +10,8 @@ data class ShowDialogInfoEvent(
data class ErrorEvent(val errorText: String) : VKEvent()
object IllegalTokenEvent : VKEvent()
data class CaptchaEvent(val sid: String, val image: String) : VKEvent()
data class ValidationEvent(val sid: String) : VKEvent()
object StartProgressEvent : VKEvent()
object StopProgressEvent : VKEvent()
@@ -2,6 +2,7 @@ package com.meloda.fast.database
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage
@@ -18,9 +19,10 @@ import com.meloda.fast.database.dao.UsersDao
VkUser::class,
VkGroup::class
],
version = 18,
exportSchema = false
version = 24,
exportSchema = false,
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun conversationsDao(): ConversationsDao
@@ -0,0 +1,95 @@
package com.meloda.fast.database
import androidx.room.TypeConverter
import com.google.gson.Gson
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.attachments.VkAttachment
import org.json.JSONObject
import java.util.stream.Collectors
class Converters {
@TypeConverter
fun fromListVkMessageToString(messages: List<VkMessage>?): String? {
if (messages == null) return null
val string =
messages.map { fromVkMessageToString(it)!! }.stream()
.collect(Collectors.joining("fastkruta228355"))
return string
}
@TypeConverter
fun fromStringToListVkMessage(string: String?): List<VkMessage>? {
if (string == null) return null
if (string.contains("fastkruta228355")) {
val messages =
string.split("fastkruta228355").map { fromStringToVkMessage(it)!! }
return messages
}
val message = fromStringToVkMessage(string)!!
return listOf(message)
}
@TypeConverter
fun fromVkMessageToString(message: VkMessage?): String? {
if (message == null) return null
return Gson().toJson(message)
}
@TypeConverter
fun fromStringToVkMessage(string: String?): VkMessage? {
if (string == null) return null
return Gson().fromJson(string, VkMessage::class.java)
}
@TypeConverter
fun fromListVkAttachmentToString(attachments: List<VkAttachment>?): String? {
if (attachments == null) return null
val string =
attachments.map { fromVkAttachmentToString(it)!! }.stream()
.collect(Collectors.joining("fastkruta228355"))
return string
}
@TypeConverter
fun fromStringToListVkAttachment(string: String?): List<VkAttachment>? {
if (string == null) return null
if (string.contains("fastkruta228355")) {
val attachments =
string.split("fastkruta228355").map { fromStringToVkAttachment(it)!! }
return attachments
}
val attachment = fromStringToVkAttachment(string)!!
return listOf(attachment)
}
@TypeConverter
fun fromVkAttachmentToString(attachment: VkAttachment?): String? {
if (attachment == null) return null
return Gson().toJson(attachment)
}
@TypeConverter
fun fromStringToVkAttachment(string: String?): VkAttachment? {
if (string == null) return null
val className = JSONObject(string).optString("className")
return Gson().fromJson(string, Class.forName(className)) as VkAttachment?
}
}
@@ -1,7 +1,26 @@
package com.meloda.fast.database.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.meloda.fast.api.model.VkMessage
@Dao
interface MessagesDao {
@Query("SELECT * FROM messages")
suspend fun getAll(): List<VkMessage>
@Query("SELECT * FROM messages WHERE id = :id")
suspend fun getById(id: Int): VkMessage?
@Query("SELECT * FROM messages WHERE peerId = :peerId")
suspend fun getByPeerId(peerId: Int): List<VkMessage>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(values: List<VkMessage>)
suspend fun insert(values: Array<out VkMessage>) = insert(values.toList())
}
@@ -3,6 +3,7 @@ package com.meloda.fast.screens.conversations
import android.os.Bundle
import android.view.View
import android.viewbinding.library.fragment.viewBinding
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
@@ -82,6 +83,8 @@ class ConversationsFragment :
return
}
binding.toolbar.overflowIcon = ContextCompat.getDrawable(requireContext(), R.drawable.test)
viewModel.loadProfileUser()
viewModel.loadConversations()
}
@@ -10,7 +10,6 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
@@ -21,9 +20,7 @@ import com.google.android.material.textfield.TextInputLayout
import com.meloda.fast.BuildConfig
import com.meloda.fast.R
import com.meloda.fast.base.BaseViewModelFragment
import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.base.viewmodel.*
import com.meloda.fast.databinding.DialogCaptchaBinding
import com.meloda.fast.databinding.DialogValidationBinding
import com.meloda.fast.databinding.FragmentLoginBinding
@@ -60,10 +57,6 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
prepareViews()
binding.loginInput.clearFocus()
setFragmentResultListener("validation") { _, bundle ->
lifecycleScope.launch { viewModel.getValidatedData(bundle) }
}
}
override fun onEvent(event: VKEvent) {
@@ -71,15 +64,13 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
when (event) {
is ShowError -> showErrorSnackbar(event.errorDescription)
is CaptchaRequired -> showCaptchaDialog(event.captcha.first, event.captcha.second)
CodeSent -> showValidationDialog()
is ValidationRequired -> showValidationRequired()
is CaptchaEvent -> showCaptchaDialog(event.sid, event.image)
is ValidationEvent -> showValidationRequired(event.sid)
is SuccessAuth -> goToMain(event.haveAuthorized)
StartProgressEvent -> onProgressStarted()
StopProgressEvent -> onProgressStopped()
is CodeSent -> showValidationDialog()
is StartProgressEvent -> onProgressStarted()
is StopProgressEvent -> onProgressStopped()
}
}
@@ -286,8 +277,9 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
validationBinding.cancel.setOnClickListener { dialog.dismiss() }
}
private fun showValidationRequired() {
private fun showValidationRequired(validationSid: String) {
Toast.makeText(requireContext(), R.string.validation_required, Toast.LENGTH_LONG).show()
viewModel.sendSms(validationSid)
}
private fun showErrorSnackbar(errorDescription: String) {
@@ -1,11 +1,9 @@
package com.meloda.fast.screens.login
import android.os.Bundle
import androidx.lifecycle.viewModelScope
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.model.request.RequestAuthDirect
import com.meloda.fast.api.network.datasource.AuthDataSource
import com.meloda.fast.base.viewmodel.*
@@ -18,8 +16,6 @@ class LoginViewModel @Inject constructor(
private val dataSource: AuthDataSource
) : BaseViewModel() {
lateinit var unknownErrorDefaultText: String
fun login(
login: String,
password: String,
@@ -52,24 +48,12 @@ class LoginViewModel @Inject constructor(
UserConfig.userId = it.userId
UserConfig.accessToken = it.accessToken
sendEvent(SuccessAuth(haveAuthorized = true))
sendEvent(SuccessAuth())
},
onError = {
if (it !is VKException) return@makeJob
twoFaCode?.let { sendEvent(CodeSent) }
if (VkUtils.isValidationRequired(it)) {
it.validationSid?.let { sid ->
sendEvent(ValidationRequired(validationSid = sid))
sendSms(sid)
}
} else if (VkUtils.isCaptchaRequired(it)) {
it.captcha?.let { captcha ->
sendEvent(CaptchaRequired(captcha.first to captcha.second))
}
}
},
onStart = { sendEvent(StartProgressEvent) },
onEnd = { sendEvent(StopProgressEvent) }
@@ -84,23 +68,10 @@ class LoginViewModel @Inject constructor(
onEnd = {})
}
suspend fun getValidatedData(bundle: Bundle) {
val accessToken = bundle.getString("token") ?: ""
val userId = bundle.getInt("userId")
UserConfig.accessToken = accessToken
UserConfig.userId = userId
tasksEventChannel.send(SuccessAuth())
}
}
data class ShowError(val errorDescription: String) : VKEvent()
data class ValidationRequired(val validationSid: String) : VKEvent()
data class CaptchaRequired(val captcha: Pair<String, String>) : VKEvent()
object CodeSent : VKEvent()
data class SuccessAuth(val haveAuthorized: Boolean = true) : VKEvent()
@@ -20,10 +20,13 @@ 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.api.model.attachments.VkSticker
import com.meloda.fast.api.model.attachments.VkVideo
import com.meloda.fast.base.adapter.BaseAdapter
import com.meloda.fast.base.adapter.BaseHolder
import com.meloda.fast.databinding.*
import com.meloda.fast.util.AndroidUtils
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.roundToInt
class MessagesHistoryAdapter constructor(
@@ -50,6 +53,8 @@ class MessagesHistoryAdapter constructor(
) return if (message.isOut) ATTACHMENT_PHOTOS_OUT
else ATTACHMENT_PHOTOS_IN
if (attachments[0] is VkVideo) return if (message.isOut) ATTACHMENT_VIDEOS_OUT
else ATTACHMENT_VIDEOS_IN
if (attachments[0] is VkSticker) return if (message.isOut) ATTACHMENT_STICKER_OUT
else ATTACHMENT_STICKER_IN
@@ -84,6 +89,9 @@ class MessagesHistoryAdapter constructor(
ATTACHMENT_PHOTOS_OUT -> AttachmentPhotosOutgoing(
ItemMessageAttachmentPhotosOutBinding.inflate(inflater, parent, false)
)
ATTACHMENT_VIDEOS_IN, ATTACHMENT_VIDEOS_OUT -> AttachmentVideosIncoming(
ItemMessageAttachmentVideosInBinding.inflate(inflater, parent, false)
)
OUTGOING -> OutgoingMessage(
ItemMessageOutBinding.inflate(inflater, parent, false)
)
@@ -215,6 +223,12 @@ class MessagesHistoryAdapter constructor(
init {
MessagesManager.setRootMaxWidth(binding.bubble)
binding.bubbleStroke.setOnClickListener { binding.bubble.performClick() }
binding.bubble.setOnClickListener {
binding.time.isVisible = !binding.time.isVisible
}
}
override fun bind(position: Int) {
@@ -238,6 +252,9 @@ class MessagesHistoryAdapter constructor(
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundStroke
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleStroke
else backgroundStroke
binding.time.text =
SimpleDateFormat("HH:mm", Locale.getDefault()).format(message.date * 1000L)
}
}
@@ -303,12 +320,52 @@ class MessagesHistoryAdapter constructor(
override fun bind(position: Int) {
val message = getItem(position)
val prevMessage = getOrNull(position - 1)
val nextMessage = getOrNull(position + 1)
val messageUser =
if (message.isUser()) profiles[message.fromId]
else null
val messageGroup =
if (message.isGroup()) groups[message.fromId]
else null
MessagesManager.loadMessageAvatar(
message = message,
messageUser = messageUser,
messageGroup = messageGroup,
imageView = binding.avatar
)
if (!message.isPeerChat()) {
binding.avatar.isVisible = false
binding.spacer.isVisible =
!(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60)
} else {
binding.spacer.isVisible =
if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat()
else message.date - prevMessage.date >= 60
binding.avatar.visibility =
if (nextMessage == null || nextMessage.fromId != message.fromId) if (message.isPeerChat()) View.VISIBLE else View.GONE
else if (nextMessage.date - message.date >= 60) View.VISIBLE
else View.INVISIBLE
}
MessagesManager.loadPhotos(
context = context,
message = message,
binding.photosContainer
photosContainer = binding.photosContainer
)
MessagesManager.setMessageText(
message = message,
textView = binding.text
)
binding.bubble.isVisible = binding.text.text.toString().isNotEmpty()
}
}
@@ -324,9 +381,66 @@ class MessagesHistoryAdapter constructor(
message = message,
photosContainer = binding.photosContainer
)
}
}
inner class AttachmentVideosIncoming(
private val binding: ItemMessageAttachmentVideosInBinding
) : Holder(binding.root) {
override fun bind(position: Int) {
val message = getItem(position)
val prevMessage = getOrNull(position - 1)
val nextMessage = getOrNull(position + 1)
val messageUser =
if (message.isUser()) profiles[message.fromId]
else null
val messageGroup =
if (message.isGroup()) groups[message.fromId]
else null
MessagesManager.loadMessageAvatar(
message = message,
messageUser = messageUser,
messageGroup = messageGroup,
imageView = binding.avatar
)
if (!message.isPeerChat()) {
binding.avatar.isVisible = false
binding.spacer.isVisible =
!(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60)
} else {
binding.spacer.isVisible =
if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat()
else message.date - prevMessage.date >= 60
binding.avatar.visibility =
if (nextMessage == null || nextMessage.fromId != message.fromId) if (message.isPeerChat()) View.VISIBLE else View.GONE
else if (nextMessage.date - message.date >= 60) View.VISIBLE
else View.INVISIBLE
}
MessagesManager.loadVideos(
context = context,
message = message,
videosContainer = binding.videosContainer
)
MessagesManager.setMessageText(
message = message,
textView = binding.text
)
binding.bubble.isVisible = binding.text.text.toString().isNotEmpty()
}
}
inner class AttachmentStickerOutgoing(
private val binding: ItemMessageAttachmentStickerOutBinding
) : Holder(binding.root) {
@@ -444,8 +558,10 @@ class MessagesHistoryAdapter constructor(
private const val ATTACHMENT_PHOTOS_IN = 101
private const val ATTACHMENT_PHOTOS_OUT = 102
private const val ATTACHMENT_STICKER_IN = 111
private const val ATTACHMENT_STICKER_OUT = 112
private const val ATTACHMENT_VIDEOS_IN = 111
private const val ATTACHMENT_VIDEOS_OUT = 112
private const val ATTACHMENT_STICKER_IN = 121
private const val ATTACHMENT_STICKER_OUT = 122
private val COMPARATOR = object : DiffUtil.ItemCallback<VkMessage>() {
override fun areItemsTheSame(
@@ -58,6 +58,8 @@ class MessagesHistoryViewModel @Inject constructor(
baseMessage.asVkMessage().let { message -> messages[message.id] = message }
}
dataSource.storeMessages(messages.values.toList())
val conversations = hashMapOf<Int, VkConversation>()
response.conversations?.let { baseConversations ->
baseConversations.forEach { baseConversation ->
@@ -1,19 +1,28 @@
package com.meloda.fast.screens.messages
import android.content.Context
import android.content.res.ColorStateList
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.Space
import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isNotEmpty
import androidx.core.view.setPadding
import coil.load
import com.google.android.material.imageview.ShapeableImageView
import com.meloda.fast.R
import com.meloda.fast.api.VkUtils
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.api.model.attachments.VkVideo
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.widget.BoundedFrameLayout
@@ -53,7 +62,7 @@ object MessagesManager {
AndroidUtils.px(size.height).roundToInt()
)
it.shapeAppearanceModel =
it.shapeAppearanceModel.withCornerSize { AndroidUtils.px(4) }
it.shapeAppearanceModel.withCornerSize { AndroidUtils.px(5) }
it.scaleType = ImageView.ScaleType.CENTER_CROP
}
@@ -69,7 +78,77 @@ object MessagesManager {
photosContainer.addView(newPhoto)
newPhoto.load(size.url)
newPhoto.load(size.url) { crossfade(100) }
}
}
}
fun loadVideos(
context: Context,
message: VkMessage,
videosContainer: LinearLayoutCompat
) {
videosContainer.removeAllViews()
val playColor = ContextCompat.getColor(context, R.color.a3_700)
val playBackgroundColor = ContextCompat.getColor(context, R.color.a3_200)
message.attachments?.let { attachments ->
val photos = attachments.map { it as VkVideo }
photos.forEach { video ->
val size = video.images[1] ?: return
val layout = FrameLayout(context).apply {
layoutParams = LinearLayoutCompat.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
val newPhoto = ShapeableImageView(context).apply {
layoutParams = FrameLayout.LayoutParams(
AndroidUtils.px(size.width).roundToInt(),
AndroidUtils.px(size.height).roundToInt()
)
shapeAppearanceModel =
shapeAppearanceModel.withCornerSize { AndroidUtils.px(5) }
scaleType = ImageView.ScaleType.CENTER_CROP
}
val play = AppCompatImageView(context).apply {
layoutParams = FrameLayout.LayoutParams(
AndroidUtils.px(50).roundToInt(),
AndroidUtils.px(50).roundToInt()
).also {
it.gravity = Gravity.CENTER
}
backgroundTintList = ColorStateList.valueOf(playBackgroundColor)
imageTintList = ColorStateList.valueOf(playColor)
setBackgroundResource(R.drawable.ic_play_button_circle_background)
setImageResource(R.drawable.ic_round_play_arrow_24)
setPadding(12)
}
layout.addView(newPhoto)
layout.addView(play)
val spacer = Space(context).apply {
layoutParams = LinearLayoutCompat.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
AndroidUtils.px(5).roundToInt()
)
}
if (videosContainer.isNotEmpty())
videosContainer.addView(spacer)
videosContainer.addView(layout)
newPhoto.load(size.url) { crossfade(100) }
}
}
}
@@ -93,7 +172,7 @@ object MessagesManager {
message: VkMessage,
textView: TextView
) {
textView.text = message.text ?: "[no_message]"
textView.text = VkUtils.prepareMessageText(message.text)
}