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 = const val USER_FIELDS =
"photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info" "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 API_VERSION = "5.132"
const val VK_APP_ID = "2274003" const val VK_APP_ID = "2274003"
const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH" const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH"
@@ -27,7 +27,9 @@ object VkUtils {
return throwable.error == VkErrors.NEED_CAPTCHA return throwable.error == VkErrors.NEED_CAPTCHA
} }
fun prepareMessageText(text: String): String { fun prepareMessageText(text: String?): String? {
if (text == null) return null
return text return text
.replace("\n", " ") .replace("\n", " ")
.replace("&amp", "&") .replace("&amp", "&")
@@ -58,9 +60,7 @@ object VkUtils {
} }
BaseVkAttachmentItem.AttachmentType.VIDEO -> { BaseVkAttachmentItem.AttachmentType.VIDEO -> {
val video = baseAttachment.video ?: continue val video = baseAttachment.video ?: continue
attachments += VkVideo( attachments += video.asVkVideo()
link = video.player
)
} }
BaseVkAttachmentItem.AttachmentType.AUDIO -> { BaseVkAttachmentItem.AttachmentType.AUDIO -> {
val audio = baseAttachment.audio ?: continue val audio = baseAttachment.audio ?: continue
@@ -1,8 +1,8 @@
package com.meloda.fast.api.model package com.meloda.fast.api.model
import android.os.Parcelable import android.os.Parcelable
import androidx.room.Embedded
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -24,10 +24,14 @@ data class VkConversation(
val lastMessageId: Int, val lastMessageId: Int,
val unreadCount: Int?, val unreadCount: Int?,
val membersCount: Int?, val membersCount: Int?,
val isPinned: Boolean val isPinned: Boolean,
) : Parcelable {
@Ignore @Embedded(prefix = "pinnedMessage_")
var pinnedMessage: VkMessage? = null,
@Embedded(prefix = "lastMessage_")
var lastMessage: VkMessage? = null var lastMessage: VkMessage? = null
) : Parcelable {
fun isChat() = type == "chat" fun isChat() = type == "chat"
fun isUser() = type == "user" fun isUser() = type == "user"
@@ -2,10 +2,8 @@ package com.meloda.fast.api.model
import android.os.Parcelable import android.os.Parcelable
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.meloda.fast.api.model.attachments.VkAttachment import com.meloda.fast.api.model.attachments.VkAttachment
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Entity(tableName = "messages") @Entity(tableName = "messages")
@@ -25,16 +23,10 @@ data class VkMessage(
val actionConversationMessageId: Int? = null, val actionConversationMessageId: Int? = null,
val actionMessage: String? = null, val actionMessage: String? = null,
val geoType: String? = null, val geoType: String? = null,
val important: Boolean = false val important: Boolean = false,
) : Parcelable { var forwards: List<VkMessage>? = null,
@IgnoredOnParcel
@Ignore
var forwards: List<VkMessage>? = null
@IgnoredOnParcel
@Ignore
var attachments: List<VkAttachment>? = null var attachments: List<VkAttachment>? = null
) : Parcelable {
fun isPeerChat() = peerId > 2_000_000_000 fun isPeerChat() = peerId > 2_000_000_000
@@ -1,5 +1,7 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import android.os.Parcelable 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 package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkAudio( data class VkAudio(
val link: String val link: String
) : VkAttachment() ) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,14 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkCall( data class VkCall(
val initiatorId: Int val initiatorId: Int
) : VkAttachment() ) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkFile( data class VkFile(
val link: String val link: String
) : VkAttachment() ) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkGift( data class VkGift(
val link: String val link: String
) : VkAttachment() ) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkGraffiti( data class VkGraffiti(
val link: String val link: String
) : VkAttachment() ) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkGroupCall( data class VkGroupCall(
val initiatorId: Int val initiatorId: Int
) : VkAttachment() ) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkLink( data class VkLink(
val link: String val link: String
) : VkAttachment() ) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkMiniApp( data class VkMiniApp(
val link: String val link: String
) : VkAttachment() ) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,6 +1,7 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.Size import com.meloda.fast.api.model.base.attachments.Size
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -16,6 +17,9 @@ data class VkPhoto(
val userId: Int? val userId: Int?
) : VkAttachment() { ) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
fun sizeOfType(type: Char): Size? { fun sizeOfType(type: Char): Size? {
for (size in sizes) { for (size in sizes) {
if (size.type == type.toString()) if (size.type == type.toString())
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkPoll( data class VkPoll(
val id: Int 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.BaseVkSticker
import com.meloda.fast.api.model.base.attachments.StickerSize import com.meloda.fast.api.model.base.attachments.StickerSize
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -12,6 +13,9 @@ data class VkSticker(
val backgroundImages: List<BaseVkSticker.Image> val backgroundImages: List<BaseVkSticker.Image>
) : VkAttachment() { ) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
fun urlForSize(@StickerSize size: Int): String? { fun urlForSize(@StickerSize size: Int): String? {
for (image in images) { for (image in images) {
if (image.width == size) return image.url if (image.width == size) return image.url
@@ -1,8 +1,21 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.BaseVkVideo
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkVideo( data class VkVideo(
val link: String val id: Int,
) : VkAttachment() 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 package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkVoiceMessage( data class VkVoiceMessage(
val link: String val link: String
) : VkAttachment() ) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkWall( data class VkWall(
val id: Int val id: Int
) : VkAttachment() ) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -1,8 +1,13 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkWallReply( data class VkWallReply(
val id: Int val id: Int
) : VkAttachment() ) : VkAttachment() {
@IgnoredOnParcel
val className: String = this::class.java.name
}
@@ -54,7 +54,10 @@ data class BaseVkConversation(
membersCount = chatSettings?.membersCount, membersCount = chatSettings?.membersCount,
ownerId = chatSettings?.ownerId, ownerId = chatSettings?.ownerId,
isPinned = sortId.majorId > 0 isPinned = sortId.majorId > 0
).apply { this.lastMessage = lastMessage } ).apply {
this.lastMessage = lastMessage
this.pinnedMessage = chatSettings?.pinnedMessage?.asVkMessage()
}
@Parcelize @Parcelize
data class Peer( data class Peer(
@@ -111,7 +114,9 @@ data class BaseVkConversation(
val isDisappearing: Boolean, val isDisappearing: Boolean,
@SerializedName("is_service") @SerializedName("is_service")
val isService: Boolean, val isService: Boolean,
val theme: String val theme: String?,
@SerializedName("pinned_message")
val pinnedMessage: BaseVkMessage?
) : Parcelable { ) : Parcelable {
@Parcelize @Parcelize
@@ -2,8 +2,10 @@ package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.attachments.VkVideo
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
//not all fields
@Parcelize @Parcelize
data class BaseVkVideo( data class BaseVkVideo(
val id: Int, val id: Int,
@@ -53,6 +55,12 @@ data class BaseVkVideo(
//ads //ads
) : BaseVkAttachment() { ) : BaseVkAttachment() {
fun asVkVideo() = VkVideo(
id = id,
images = image,
firstFrames = firstFrame
)
@Parcelize @Parcelize
data class Image( data class Image(
val height: Int, val height: Int,
@@ -1,9 +1,11 @@
package com.meloda.fast.api.network package com.meloda.fast.api.network
import com.meloda.fast.api.VKException
import com.meloda.fast.api.base.ApiResponse import com.meloda.fast.api.base.ApiResponse
import okhttp3.Request import okhttp3.Request
import okio.IOException import okio.IOException
import okio.Timeout import okio.Timeout
import org.json.JSONObject
import retrofit2.* import retrofit2.*
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type 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() ?: "")) } else Answer.Error(IOException(response.errorBody()?.string() ?: ""))
if (result is Answer.Error) if (checkErrors(call, result)) return
callback.onResponse(proxy, Response.success(result)) 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)) 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 { override fun timeout(): Timeout {
@@ -1,5 +1,6 @@
package com.meloda.fast.api.network.datasource 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.MessagesGetHistoryRequest
import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest
import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest
@@ -25,4 +26,8 @@ class MessagesDataSource @Inject constructor(
suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) = suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) =
repo.getLongPollServer(params.map) 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 package com.meloda.fast.base.viewmodel
import android.util.Log
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.meloda.fast.api.VKException import com.meloda.fast.api.VKException
@@ -14,6 +13,8 @@ import kotlinx.coroutines.launch
abstract class BaseViewModel : ViewModel() { abstract class BaseViewModel : ViewModel() {
var unknownErrorDefaultText: String = ""
protected val tasksEventChannel = Channel<VKEvent>() protected val tasksEventChannel = Channel<VKEvent>()
val tasksEvent = tasksEventChannel.receiveAsFlow() val tasksEvent = tasksEventChannel.receiveAsFlow()
@@ -30,6 +31,12 @@ abstract class BaseViewModel : ViewModel() {
is Answer.Error -> { is Answer.Error -> {
checkErrors(response.throwable) checkErrors(response.throwable)
onError?.invoke(response.throwable) onError?.invoke(response.throwable)
?: sendEvent(
ErrorEvent(
response.throwable.message
?: unknownErrorDefaultText
)
)
} }
} }
}.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } } }.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) protected suspend fun <T : VKEvent> sendEvent(event: T) = tasksEventChannel.send(event)
private suspend fun checkErrors(throwable: Throwable) { private suspend fun checkErrors(throwable: Throwable) {
if (throwable is ApiError) { when (throwable) {
is ApiError -> {
when (throwable.errorCode) { when (throwable.errorCode) {
VkErrorCodes.USER_AUTHORIZATION_FAILED -> { VkErrorCodes.USER_AUTHORIZATION_FAILED -> {
sendEvent(IllegalTokenEvent) sendEvent(IllegalTokenEvent)
return
} }
} }
} else if (throwable is VKException) { }
is VKException -> {
when (throwable.error) { when (throwable.error) {
VkErrors.NEED_CAPTCHA -> { VkErrors.NEED_CAPTCHA -> {
throwable.captcha = val json = throwable.json ?: return
(throwable.json?.optString("captcha_sid") sendEvent(
?: "") to (throwable.json?.optString("captcha_img") ?: "") CaptchaEvent(
return sid = json.optString("captcha_sid"),
image = json.optString("captcha_img")
)
)
} }
VkErrors.NEED_VALIDATION -> { VkErrors.NEED_VALIDATION -> {
throwable.validationSid = throwable.json?.optString("validation_sid") val json = throwable.json ?: return
return sendEvent(ValidationEvent(sid = json.optString("validation_sid")))
}
}
} }
} }
} }
sendEvent(ShowDialogInfoEvent(null, Log.getStackTraceString(throwable)))
}
} }
@@ -10,6 +10,8 @@ data class ShowDialogInfoEvent(
data class ErrorEvent(val errorText: String) : VKEvent() data class ErrorEvent(val errorText: String) : VKEvent()
object IllegalTokenEvent : 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 StartProgressEvent : VKEvent()
object StopProgressEvent : VKEvent() object StopProgressEvent : VKEvent()
@@ -2,6 +2,7 @@ package com.meloda.fast.database
import androidx.room.Database import androidx.room.Database
import androidx.room.RoomDatabase import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkMessage
@@ -18,9 +19,10 @@ import com.meloda.fast.database.dao.UsersDao
VkUser::class, VkUser::class,
VkGroup::class VkGroup::class
], ],
version = 18, version = 24,
exportSchema = false exportSchema = false,
) )
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun conversationsDao(): ConversationsDao 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 package com.meloda.fast.database.dao
import androidx.room.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 @Dao
interface MessagesDao { 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.os.Bundle
import android.view.View import android.view.View
import android.viewbinding.library.fragment.viewBinding import android.viewbinding.library.fragment.viewBinding
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
@@ -82,6 +83,8 @@ class ConversationsFragment :
return return
} }
binding.toolbar.overflowIcon = ContextCompat.getDrawable(requireContext(), R.drawable.test)
viewModel.loadProfileUser() viewModel.loadProfileUser()
viewModel.loadConversations() viewModel.loadConversations()
} }
@@ -10,7 +10,6 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.setFragmentResultListener
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController 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.BuildConfig
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.base.BaseViewModelFragment import com.meloda.fast.base.BaseViewModelFragment
import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.*
import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.databinding.DialogCaptchaBinding import com.meloda.fast.databinding.DialogCaptchaBinding
import com.meloda.fast.databinding.DialogValidationBinding import com.meloda.fast.databinding.DialogValidationBinding
import com.meloda.fast.databinding.FragmentLoginBinding import com.meloda.fast.databinding.FragmentLoginBinding
@@ -60,10 +57,6 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
prepareViews() prepareViews()
binding.loginInput.clearFocus() binding.loginInput.clearFocus()
setFragmentResultListener("validation") { _, bundle ->
lifecycleScope.launch { viewModel.getValidatedData(bundle) }
}
} }
override fun onEvent(event: VKEvent) { override fun onEvent(event: VKEvent) {
@@ -71,15 +64,13 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
when (event) { when (event) {
is ShowError -> showErrorSnackbar(event.errorDescription) is ShowError -> showErrorSnackbar(event.errorDescription)
is CaptchaRequired -> showCaptchaDialog(event.captcha.first, event.captcha.second) is CaptchaEvent -> showCaptchaDialog(event.sid, event.image)
is ValidationEvent -> showValidationRequired(event.sid)
CodeSent -> showValidationDialog()
is ValidationRequired -> showValidationRequired()
is SuccessAuth -> goToMain(event.haveAuthorized) is SuccessAuth -> goToMain(event.haveAuthorized)
StartProgressEvent -> onProgressStarted() is CodeSent -> showValidationDialog()
StopProgressEvent -> onProgressStopped() is StartProgressEvent -> onProgressStarted()
is StopProgressEvent -> onProgressStopped()
} }
} }
@@ -286,8 +277,9 @@ class LoginFragment : BaseViewModelFragment<LoginViewModel>(R.layout.fragment_lo
validationBinding.cancel.setOnClickListener { dialog.dismiss() } validationBinding.cancel.setOnClickListener { dialog.dismiss() }
} }
private fun showValidationRequired() { private fun showValidationRequired(validationSid: String) {
Toast.makeText(requireContext(), R.string.validation_required, Toast.LENGTH_LONG).show() Toast.makeText(requireContext(), R.string.validation_required, Toast.LENGTH_LONG).show()
viewModel.sendSms(validationSid)
} }
private fun showErrorSnackbar(errorDescription: String) { private fun showErrorSnackbar(errorDescription: String) {
@@ -1,11 +1,9 @@
package com.meloda.fast.screens.login package com.meloda.fast.screens.login
import android.os.Bundle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.meloda.fast.api.UserConfig import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.VKException 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.model.request.RequestAuthDirect
import com.meloda.fast.api.network.datasource.AuthDataSource import com.meloda.fast.api.network.datasource.AuthDataSource
import com.meloda.fast.base.viewmodel.* import com.meloda.fast.base.viewmodel.*
@@ -18,8 +16,6 @@ class LoginViewModel @Inject constructor(
private val dataSource: AuthDataSource private val dataSource: AuthDataSource
) : BaseViewModel() { ) : BaseViewModel() {
lateinit var unknownErrorDefaultText: String
fun login( fun login(
login: String, login: String,
password: String, password: String,
@@ -52,24 +48,12 @@ class LoginViewModel @Inject constructor(
UserConfig.userId = it.userId UserConfig.userId = it.userId
UserConfig.accessToken = it.accessToken UserConfig.accessToken = it.accessToken
sendEvent(SuccessAuth(haveAuthorized = true)) sendEvent(SuccessAuth())
}, },
onError = { onError = {
if (it !is VKException) return@makeJob if (it !is VKException) return@makeJob
twoFaCode?.let { sendEvent(CodeSent) } 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) }, onStart = { sendEvent(StartProgressEvent) },
onEnd = { sendEvent(StopProgressEvent) } onEnd = { sendEvent(StopProgressEvent) }
@@ -84,23 +68,10 @@ class LoginViewModel @Inject constructor(
onEnd = {}) 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 ShowError(val errorDescription: String) : VKEvent()
data class ValidationRequired(val validationSid: String) : VKEvent()
data class CaptchaRequired(val captcha: Pair<String, String>) : VKEvent()
object CodeSent : VKEvent() object CodeSent : VKEvent()
data class SuccessAuth(val haveAuthorized: Boolean = true) : 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.VkUser
import com.meloda.fast.api.model.attachments.VkPhoto import com.meloda.fast.api.model.attachments.VkPhoto
import com.meloda.fast.api.model.attachments.VkSticker 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.BaseAdapter
import com.meloda.fast.base.adapter.BaseHolder import com.meloda.fast.base.adapter.BaseHolder
import com.meloda.fast.databinding.* import com.meloda.fast.databinding.*
import com.meloda.fast.util.AndroidUtils import com.meloda.fast.util.AndroidUtils
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
class MessagesHistoryAdapter constructor( class MessagesHistoryAdapter constructor(
@@ -50,6 +53,8 @@ class MessagesHistoryAdapter constructor(
) return if (message.isOut) ATTACHMENT_PHOTOS_OUT ) return if (message.isOut) ATTACHMENT_PHOTOS_OUT
else ATTACHMENT_PHOTOS_IN 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 if (attachments[0] is VkSticker) return if (message.isOut) ATTACHMENT_STICKER_OUT
else ATTACHMENT_STICKER_IN else ATTACHMENT_STICKER_IN
@@ -84,6 +89,9 @@ class MessagesHistoryAdapter constructor(
ATTACHMENT_PHOTOS_OUT -> AttachmentPhotosOutgoing( ATTACHMENT_PHOTOS_OUT -> AttachmentPhotosOutgoing(
ItemMessageAttachmentPhotosOutBinding.inflate(inflater, parent, false) ItemMessageAttachmentPhotosOutBinding.inflate(inflater, parent, false)
) )
ATTACHMENT_VIDEOS_IN, ATTACHMENT_VIDEOS_OUT -> AttachmentVideosIncoming(
ItemMessageAttachmentVideosInBinding.inflate(inflater, parent, false)
)
OUTGOING -> OutgoingMessage( OUTGOING -> OutgoingMessage(
ItemMessageOutBinding.inflate(inflater, parent, false) ItemMessageOutBinding.inflate(inflater, parent, false)
) )
@@ -215,6 +223,12 @@ class MessagesHistoryAdapter constructor(
init { init {
MessagesManager.setRootMaxWidth(binding.bubble) MessagesManager.setRootMaxWidth(binding.bubble)
binding.bubbleStroke.setOnClickListener { binding.bubble.performClick() }
binding.bubble.setOnClickListener {
binding.time.isVisible = !binding.time.isVisible
}
} }
override fun bind(position: Int) { override fun bind(position: Int) {
@@ -238,6 +252,9 @@ class MessagesHistoryAdapter constructor(
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundStroke if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundStroke
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleStroke else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleStroke
else backgroundStroke 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) { override fun bind(position: Int) {
val message = getItem(position) 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( MessagesManager.loadPhotos(
context = context, context = context,
message = message, 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, message = message,
photosContainer = binding.photosContainer 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( inner class AttachmentStickerOutgoing(
private val binding: ItemMessageAttachmentStickerOutBinding private val binding: ItemMessageAttachmentStickerOutBinding
) : Holder(binding.root) { ) : Holder(binding.root) {
@@ -444,8 +558,10 @@ class MessagesHistoryAdapter constructor(
private const val ATTACHMENT_PHOTOS_IN = 101 private const val ATTACHMENT_PHOTOS_IN = 101
private const val ATTACHMENT_PHOTOS_OUT = 102 private const val ATTACHMENT_PHOTOS_OUT = 102
private const val ATTACHMENT_STICKER_IN = 111 private const val ATTACHMENT_VIDEOS_IN = 111
private const val ATTACHMENT_STICKER_OUT = 112 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>() { private val COMPARATOR = object : DiffUtil.ItemCallback<VkMessage>() {
override fun areItemsTheSame( override fun areItemsTheSame(
@@ -58,6 +58,8 @@ class MessagesHistoryViewModel @Inject constructor(
baseMessage.asVkMessage().let { message -> messages[message.id] = message } baseMessage.asVkMessage().let { message -> messages[message.id] = message }
} }
dataSource.storeMessages(messages.values.toList())
val conversations = hashMapOf<Int, VkConversation>() val conversations = hashMapOf<Int, VkConversation>()
response.conversations?.let { baseConversations -> response.conversations?.let { baseConversations ->
baseConversations.forEach { baseConversation -> baseConversations.forEach { baseConversation ->
@@ -1,19 +1,28 @@
package com.meloda.fast.screens.messages package com.meloda.fast.screens.messages
import android.content.Context import android.content.Context
import android.content.res.ColorStateList
import android.view.Gravity
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.Space import android.widget.Space
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.LinearLayoutCompat import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isNotEmpty import androidx.core.view.isNotEmpty
import androidx.core.view.setPadding
import coil.load import coil.load
import com.google.android.material.imageview.ShapeableImageView 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.VkGroup
import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.attachments.VkPhoto 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.common.AppGlobal
import com.meloda.fast.util.AndroidUtils import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.widget.BoundedFrameLayout import com.meloda.fast.widget.BoundedFrameLayout
@@ -53,7 +62,7 @@ object MessagesManager {
AndroidUtils.px(size.height).roundToInt() AndroidUtils.px(size.height).roundToInt()
) )
it.shapeAppearanceModel = it.shapeAppearanceModel =
it.shapeAppearanceModel.withCornerSize { AndroidUtils.px(4) } it.shapeAppearanceModel.withCornerSize { AndroidUtils.px(5) }
it.scaleType = ImageView.ScaleType.CENTER_CROP it.scaleType = ImageView.ScaleType.CENTER_CROP
} }
@@ -69,7 +78,77 @@ object MessagesManager {
photosContainer.addView(newPhoto) 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, message: VkMessage,
textView: TextView textView: TextView
) { ) {
textView.text = message.text ?: "[no_message]" textView.text = VkUtils.prepareMessageText(message.text)
} }
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/white" />
</shape>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M8,6.82v10.36c0,0.79 0.87,1.27 1.54,0.84l8.14,-5.18c0.62,-0.39 0.62,-1.29 0,-1.69L9.54,5.98C8.87,5.55 8,6.03 8,6.82z"/>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

@@ -20,7 +20,7 @@
android:elevation="0dp" android:elevation="0dp"
app:collapsedTitleTextAppearance="@style/CollapsingToolbarCollapsedTitle" app:collapsedTitleTextAppearance="@style/CollapsingToolbarCollapsedTitle"
app:expandedTitleTextAppearance="@style/CollapsingToolbarTitle" app:expandedTitleTextAppearance="@style/CollapsingToolbarTitle"
app:layout_scrollFlags="scroll|enterAlwaysCollapsed|snap" app:layout_scrollFlags="scroll|enterAlways|snap"
app:title="Messages"> app:title="Messages">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
@@ -68,16 +68,14 @@
</com.meloda.fast.widget.BoundedFrameLayout> </com.meloda.fast.widget.BoundedFrameLayout>
<com.meloda.fast.widget.CircleImageView
android:id="@+id/unread"
android:layout_width="13dp"
android:layout_height="13dp"
android:layout_marginStart="12dp"
android:layout_marginBottom="20dp"
android:src="@color/a3_200" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/photosContainer" android:id="@+id/photosContainer"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@@ -85,8 +83,17 @@
android:layout_marginTop="5dp" android:layout_marginTop="5dp"
android:orientation="vertical" /> android:orientation="vertical" />
</androidx.appcompat.widget.LinearLayoutCompat> <com.meloda.fast.widget.CircleImageView
</androidx.appcompat.widget.LinearLayoutCompat> android:id="@+id/unread"
</androidx.appcompat.widget.LinearLayoutCompat> android:layout_width="13dp"
android:layout_height="13dp"
android:layout_gravity="bottom"
android:layout_marginStart="12dp"
android:layout_marginBottom="20dp"
android:src="@color/a3_200" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout> </layout>
@@ -0,0 +1,99 @@
<?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:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="2.5dp">
<com.meloda.fast.widget.CircleImageView
android:id="@+id/avatar"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_gravity="bottom"
android:layout_marginEnd="12dp"
tools:src="@tools:sample/avatars" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<Space
android:id="@+id/spacer"
android:layout_width="match_parent"
android:layout_height="10dp"
android:visibility="gone" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:layout_marginTop="8dp"
android:fontFamily="@font/google_sans_regular"
android:textColor="@color/a3_700"
tools:text="@tools:sample/full_names" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="bottom"
android:orientation="horizontal">
<com.meloda.fast.widget.BoundedFrameLayout
android:id="@+id/bubble"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/ic_message_in_background"
android:backgroundTint="@color/n2_100"
tools:ignore="UselessParent">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:padding="15dp"
android:textColor="@color/n1_800"
tools:text="This" />
</com.meloda.fast.widget.BoundedFrameLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/videosContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"
android:orientation="vertical" />
<com.meloda.fast.widget.CircleImageView
android:id="@+id/unread"
android:layout_width="13dp"
android:layout_height="13dp"
android:layout_gravity="bottom"
android:layout_marginStart="12dp"
android:layout_marginBottom="20dp"
android:src="@color/a3_200" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
@@ -21,6 +21,7 @@
<androidx.appcompat.widget.LinearLayoutCompat <androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical"> android:orientation="vertical">
<Space <Space
@@ -57,8 +58,19 @@
</FrameLayout> </FrameLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginEnd="12dp"
android:textColor="?textColorSecondaryVariant"
tools:layout_height="18dp"
tools:text="12:00" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout> </layout>
+1
View File
@@ -24,6 +24,7 @@
<color name="textColorSecondaryVariant">@color/n2_500</color> <color name="textColorSecondaryVariant">@color/n2_500</color>
<color name="textColorService">@color/n2_600</color> <color name="textColorService">@color/n2_600</color>
<color name="colorSurface">@color/a1_0</color> <color name="colorSurface">@color/a1_0</color>
<color name="messageOutStrokeColor">@color/n2_100</color> <color name="messageOutStrokeColor">@color/n2_100</color>