simplifying base models

new attachments
This commit is contained in:
2021-09-24 10:55:07 +03:00
parent 56fb93d2e4
commit e127501889
68 changed files with 1933 additions and 1559 deletions
-9
View File
@@ -28,15 +28,6 @@
</intent-filter>
</activity>
<receiver
android:name=".receiver.MinuteReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.ACTION_TIME_CHANGED" />
</intent-filter>
</receiver>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
@@ -2,6 +2,7 @@ package com.meloda.fast.activity
import android.os.Bundle
import android.viewbinding.library.activity.viewBinding
import androidx.lifecycle.lifecycleScope
import com.meloda.fast.R
import com.meloda.fast.base.BaseActivity
import com.meloda.fast.databinding.ActivityMainBinding
@@ -14,6 +15,10 @@ class MainActivity : BaseActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launchWhenStarted {
}
}
}
+159 -224
View File
@@ -13,27 +13,22 @@ import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.attachments.*
import com.meloda.fast.api.model.base.BaseVkMessage
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
import com.meloda.fast.api.network.VkErrors
object VkUtils {
fun isValidationRequired(throwable: Throwable): Boolean {
if (throwable !is VKException) return false
return throwable.error == VkErrors.NEED_VALIDATION
fun prepareMessageText(text: String, forConversations: Boolean? = null): String {
return text.apply {
if (forConversations == true) replace("\n", "")
replace("&amp", "&")
}
}
fun isCaptchaRequired(throwable: Throwable): Boolean {
if (throwable !is VKException) return false
return throwable.error == VkErrors.NEED_CAPTCHA
}
fun isPreviousMessageSentFiveMinutesAgo(prevMessage: VkMessage?, message: VkMessage?) =
prevMessage != null && message != null && (message.date - prevMessage.date >= 300)
fun prepareMessageText(text: String?): String? {
if (text == null) return null
return text
.replace("\n", " ")
.replace("&amp", "&")
}
fun isPreviousMessageFromDifferentSender(prevMessage: VkMessage?, message: VkMessage?) =
prevMessage != null && message != null && prevMessage.fromId != message.fromId
fun parseForwards(baseForwards: List<BaseVkMessage>?): List<VkMessage>? {
if (baseForwards.isNullOrEmpty()) return null
@@ -64,21 +59,15 @@ object VkUtils {
}
BaseVkAttachmentItem.AttachmentType.AUDIO -> {
val audio = baseAttachment.audio ?: continue
attachments += VkAudio(
link = audio.url
)
attachments += audio.asVkAudio()
}
BaseVkAttachmentItem.AttachmentType.FILE -> {
val file = baseAttachment.file ?: continue
attachments += VkFile(
link = file.url
)
attachments += file.asVkFile()
}
BaseVkAttachmentItem.AttachmentType.LINK -> {
val link = baseAttachment.link ?: continue
attachments += VkLink(
link = link.url
)
attachments += link.asVkLink()
}
BaseVkAttachmentItem.AttachmentType.MINI_APP -> {
val miniApp = baseAttachment.miniApp ?: continue
@@ -89,7 +78,7 @@ object VkUtils {
BaseVkAttachmentItem.AttachmentType.VOICE -> {
val voiceMessage = baseAttachment.voiceMessage ?: continue
attachments += VkVoiceMessage(
link = voiceMessage.linkMp3
link = voiceMessage.link_mp3
)
}
BaseVkAttachmentItem.AttachmentType.STICKER -> {
@@ -99,14 +88,12 @@ object VkUtils {
BaseVkAttachmentItem.AttachmentType.GIFT -> {
val gift = baseAttachment.gift ?: continue
attachments += VkGift(
link = gift.thumb48
link = gift.thumb_48
)
}
BaseVkAttachmentItem.AttachmentType.WALL -> {
val wall = baseAttachment.wall ?: continue
attachments += VkWall(
id = wall.id
)
attachments += wall.asVkWall()
}
BaseVkAttachmentItem.AttachmentType.GRAFFITI -> {
val graffiti = baseAttachment.graffiti ?: continue
@@ -129,15 +116,27 @@ object VkUtils {
BaseVkAttachmentItem.AttachmentType.CALL -> {
val call = baseAttachment.call ?: continue
attachments += VkCall(
initiatorId = call.initiatorId
initiatorId = call.initiator_id
)
}
BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> {
val groupCall = baseAttachment.groupCall ?: continue
attachments += VkGroupCall(
initiatorId = groupCall.initiatorId
initiatorId = groupCall.initiator_id
)
}
BaseVkAttachmentItem.AttachmentType.CURATOR -> {
val curator = baseAttachment.curator ?: continue
attachments += curator.asVkCurator()
}
BaseVkAttachmentItem.AttachmentType.EVENT -> {
val event = baseAttachment.event ?: continue
attachments += event.asVkEvent()
}
BaseVkAttachmentItem.AttachmentType.STORY -> {
val story = baseAttachment.story ?: continue
attachments += story.asVkStory()
}
else -> continue
}
}
@@ -145,177 +144,12 @@ object VkUtils {
return attachments
}
fun getActionConversationText(
message: VkMessage,
youPrefix: String,
profiles: HashMap<Int, VkUser>? = null,
groups: HashMap<Int, VkGroup>? = null,
messageUser: VkUser? = null,
messageGroup: VkGroup? = null
): String? {
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
"$prefix created «$text»"
}
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
"$prefix renamed chat to «$text»"
}
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
"$prefix updated the chat photo"
}
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
"$prefix deleted the chat photo"
}
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()
"$prefix left the chat"
} 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()
"$prefix kicked $postfix"
}
}
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()
"$prefix returned the chat"
} 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()
"$prefix invited $postfix"
}
}
VkMessage.Action.CHAT_INVITE_USER_BY_LINK -> {
val prefix = when {
message.fromId == UserConfig.userId -> youPrefix
message.isUser() -> messageUser?.toString()
else -> return null
} ?: return null
"$prefix joined the chat via link"
}
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
"$prefix joined the call via link"
}
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
"$prefix pinned message ${if (actionMessage == null) "" else "«$actionMessage»"}".trim()
}
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
"$prefix unpinned message"
}
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
"$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(
context: Context,
message: VkMessage,
youPrefix: String,
profiles: HashMap<Int, VkUser>? = null,
groups: HashMap<Int, VkGroup>? = null,
profiles: Map<Int, VkUser>? = null,
groups: Map<Int, VkGroup>? = null,
messageUser: VkUser? = null,
messageGroup: VkGroup? = null
): SpannableString? {
@@ -330,7 +164,8 @@ object VkUtils {
else -> return null
} ?: return null
val spanText = "$prefix created «$text»"
val spanText =
context.getString(R.string.message_action_chat_created, prefix, text)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
@@ -351,8 +186,8 @@ object VkUtils {
else -> return null
} ?: return null
val spanText = "$prefix renamed chat to «$text»"
val spanText =
context.getString(R.string.message_action_chat_renamed, prefix, text)
val startIndex = spanText.indexOf(text)
SpannableString(spanText).also {
@@ -370,7 +205,9 @@ object VkUtils {
else -> return null
} ?: return null
val spanText = "$prefix updated the chat photo"
val spanText =
context.getString(R.string.message_action_chat_photo_update, prefix)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
@@ -383,7 +220,9 @@ object VkUtils {
else -> return null
} ?: return null
val spanText = "$prefix deleted the chat photo"
val spanText =
context.getString(R.string.message_action_chat_photo_remove, prefix)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
@@ -402,7 +241,10 @@ object VkUtils {
if (memberId == message.fromId) {
val prefix = if (memberId == UserConfig.userId) youPrefix
else actionUser.toString()
val spanText = "$prefix left the chat"
val spanText =
context.getString(R.string.message_action_chat_user_left, prefix)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
@@ -410,11 +252,17 @@ object VkUtils {
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 spanText =
context.getString(
R.string.message_action_chat_user_kicked,
prefix,
postfix
)
val startIndex = spanText.indexOf(postfix)
SpannableString(spanText).also {
@@ -439,18 +287,27 @@ object VkUtils {
if (memberId == message.fromId) {
val prefix = if (memberId == UserConfig.userId) youPrefix
else actionUser.toString()
val spanText = "$prefix returned the chat"
val spanText =
context.getString(R.string.message_action_chat_user_returned, prefix)
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 spanText =
context.getString(
R.string.message_action_chat_user_invited,
prefix,
postfix
)
val startIndex = spanText.indexOf(postfix)
SpannableString(spanText).also {
@@ -468,7 +325,9 @@ object VkUtils {
else -> return null
} ?: return null
val spanText = "$prefix joined the chat via link"
val spanText =
context.getString(R.string.message_action_chat_user_joined_by_link, prefix)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
@@ -480,7 +339,9 @@ object VkUtils {
else -> return null
} ?: return null
val spanText = "$prefix joined the call via link"
val spanText =
context.getString(R.string.message_action_chat_user_joined_by_call_link, prefix)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
@@ -493,16 +354,11 @@ object VkUtils {
else -> return null
} ?: return null
val actionMessage = message.actionMessage ?: return null
val spanText = "$prefix pinned message «$actionMessage»"
val startIndex = spanText.indexOf(actionMessage)
val spanText =
context.getString(R.string.message_action_chat_pin_message, prefix).trim()
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 -> {
@@ -513,7 +369,9 @@ object VkUtils {
else -> return null
} ?: return null
val spanText = "$prefix unpinned message"
val spanText =
context.getString(R.string.message_action_chat_unpin_message, prefix)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
@@ -526,7 +384,9 @@ object VkUtils {
else -> return null
} ?: return null
val spanText = "$prefix took a screenshot"
val spanText =
context.getString(R.string.message_action_chat_screenshot, prefix)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
@@ -538,7 +398,9 @@ object VkUtils {
else -> return null
} ?: return null
val spanText = "$prefix changed chat theme"
val spanText =
context.getString(R.string.message_action_chat_style_update, prefix)
SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
}
@@ -548,6 +410,26 @@ object VkUtils {
}
}
fun getActionConversationText(
context: Context,
message: VkMessage,
youPrefix: String,
profiles: HashMap<Int, VkUser>? = null,
groups: HashMap<Int, VkGroup>? = null,
messageUser: VkUser? = null,
messageGroup: VkGroup? = null
): String? {
return getActionMessageText(
context = context,
message = message,
youPrefix = youPrefix,
profiles = profiles,
groups = groups,
messageUser = messageUser,
messageGroup = messageGroup
)?.toString()
}
fun getForwardsConversationText(context: Context, message: VkMessage): String? {
if (message.forwards.isNullOrEmpty()) return null
@@ -570,10 +452,19 @@ object VkUtils {
return message.attachments?.let { attachments ->
if (attachments.size == 1) {
getAttachmentTypeByClass(attachments[0])?.let { getAttachmentTextByType(it) }
getAttachmentTypeByClass(attachments[0])?.let {
getAttachmentTextByType(
context,
it
)
}
} else {
if (isAttachmentsHaveOneType(attachments)) {
getAttachmentTypeByClass(attachments[0])?.let { getAttachmentTextByType(it) }
getAttachmentTypeByClass(attachments[0])?.let {
getAttachmentTextByType(
context, it, attachments.size
)
}
} else {
context.getString(R.string.message_attachments_many)
}
@@ -622,6 +513,8 @@ object VkUtils {
BaseVkAttachmentItem.AttachmentType.WALL_REPLY -> R.drawable.ic_attachment_wall_reply
BaseVkAttachmentItem.AttachmentType.CALL -> R.drawable.ic_attachment_call
BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> R.drawable.ic_attachment_group_call
BaseVkAttachmentItem.AttachmentType.STORY -> R.drawable.ic_attachment_story
else -> return null
}
return ContextCompat.getDrawable(context, resId)
@@ -657,14 +550,56 @@ object VkUtils {
is VkWallReply -> BaseVkAttachmentItem.AttachmentType.WALL_REPLY
is VkCall -> BaseVkAttachmentItem.AttachmentType.CALL
is VkGroupCall -> BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS
is VkEvent -> BaseVkAttachmentItem.AttachmentType.EVENT
is VkCurator -> BaseVkAttachmentItem.AttachmentType.CURATOR
is VkStory -> BaseVkAttachmentItem.AttachmentType.STORY
else -> null
}
}
fun getAttachmentTextByType(attachmentType: BaseVkAttachmentItem.AttachmentType): String? {
fun getAttachmentTextByType(
context: Context,
attachmentType: BaseVkAttachmentItem.AttachmentType,
size: Int = 1
): String {
return when (attachmentType) {
BaseVkAttachmentItem.AttachmentType.PHOTO ->
context.resources.getQuantityString(R.plurals.attachment_photos, size, size)
BaseVkAttachmentItem.AttachmentType.VIDEO ->
context.resources.getQuantityString(R.plurals.attachment_videos, size, size)
BaseVkAttachmentItem.AttachmentType.AUDIO ->
context.resources.getQuantityString(R.plurals.attachment_audios, size, size)
BaseVkAttachmentItem.AttachmentType.FILE ->
context.resources.getQuantityString(R.plurals.attachment_files, size, size)
BaseVkAttachmentItem.AttachmentType.LINK ->
context.resources.getString(R.string.message_attachments_link)
BaseVkAttachmentItem.AttachmentType.VOICE ->
context.resources.getString(R.string.message_attachments_voice)
BaseVkAttachmentItem.AttachmentType.MINI_APP ->
context.resources.getString(R.string.message_attachments_mini_app)
BaseVkAttachmentItem.AttachmentType.STICKER ->
context.resources.getString(R.string.message_attachments_sticker)
BaseVkAttachmentItem.AttachmentType.GIFT ->
context.resources.getString(R.string.message_attachments_gift)
BaseVkAttachmentItem.AttachmentType.WALL ->
context.resources.getString(R.string.message_attachments_wall)
BaseVkAttachmentItem.AttachmentType.GRAFFITI ->
context.resources.getString(R.string.message_attachments_graffiti)
BaseVkAttachmentItem.AttachmentType.POLL ->
context.resources.getString(R.string.message_attachments_poll)
BaseVkAttachmentItem.AttachmentType.WALL_REPLY ->
context.resources.getString(R.string.message_attachments_wall_reply)
BaseVkAttachmentItem.AttachmentType.CALL ->
context.resources.getString(R.string.message_attachments_call)
BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS ->
context.resources.getString(R.string.message_attachments_call_in_progress)
BaseVkAttachmentItem.AttachmentType.EVENT ->
context.resources.getString(R.string.message_attachments_event)
BaseVkAttachmentItem.AttachmentType.CURATOR ->
context.resources.getString(R.string.message_attachments_curator)
BaseVkAttachmentItem.AttachmentType.STORY ->
context.resources.getString(R.string.message_attachments_story)
else -> attachmentType.value
}
}
}
@@ -5,7 +5,11 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class VkAudio(
val link: String
val id: Int,
val title: String,
val artist: String,
val url: String,
val duration: Int
) : VkAttachment() {
@IgnoredOnParcel
@@ -0,0 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkCurator(
val id: Int
) : VkAttachment()
@@ -0,0 +1,8 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkEvent(
val id: Int
) : VkAttachment()
@@ -5,7 +5,11 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class VkFile(
val link: String
val id: Int,
val title: String,
val ext: String,
val size: Int,
val url: String
) : VkAttachment() {
@IgnoredOnParcel
@@ -5,7 +5,12 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class VkLink(
val link: String
val url: String,
val title: String?,
val caption: String?,
val photo: VkPhoto?,
val target: String,
val isFavorite: Boolean
) : VkAttachment() {
@IgnoredOnParcel
@@ -1,6 +1,6 @@
package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.Size
import com.meloda.fast.api.model.base.attachments.BaseVkPhoto
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@@ -12,7 +12,7 @@ data class VkPhoto(
val ownerId: Int,
val hasTags: Boolean,
val accessKey: String?,
val sizes: List<Size>,
val sizes: List<BaseVkPhoto.Size>,
val text: String,
val userId: Int?
) : VkAttachment() {
@@ -20,7 +20,7 @@ data class VkPhoto(
@IgnoredOnParcel
val className: String = this::class.java.name
fun sizeOfType(type: Char): Size? {
fun sizeOfType(type: Char): BaseVkPhoto.Size? {
for (size in sizes) {
if (size.type == type.toString())
return size
@@ -1,7 +1,6 @@
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
@@ -16,7 +15,7 @@ data class VkSticker(
@IgnoredOnParcel
val className: String = this::class.java.name
fun urlForSize(@StickerSize size: Int): String? {
fun urlForSize(size: Int): String? {
for (image in images) {
if (image.width == size) return image.url
}
@@ -0,0 +1,11 @@
package com.meloda.fast.api.model.attachments
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkStory(
val id: Int,
val ownerId: Int,
val date: Int,
val photo: VkPhoto
) : VkAttachment()
@@ -8,7 +8,7 @@ import kotlinx.parcelize.Parcelize
data class VkVideo(
val id: Int,
val images: List<BaseVkVideo.Image>,
val firstFrames: List<BaseVkVideo.FirstFrame>
val firstFrames: List<BaseVkVideo.FirstFrame>?
) : VkAttachment() {
@IgnoredOnParcel
@@ -1,11 +1,23 @@
package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
data class VkWall(
val id: Int
val id: Int,
val fromId: Int,
val toId: Int,
val date: Int,
val text: String,
val attachments: List<BaseVkAttachmentItem>?,
val comments: Int,
val likes: Int,
val reposts: Int,
val views: Int,
val isFavorite: Boolean,
val accessKey: String
) : VkAttachment() {
@IgnoredOnParcel
@@ -1,90 +1,70 @@
package com.meloda.fast.api.model.base
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.base.attachments.BaseVkGroupCall
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkConversation(
val peer: Peer,
@SerializedName("last_message_id")
val lastMessageId: Int,
@SerializedName("in_read")
val inRead: Int,
@SerializedName("out_read")
val outRead: Int,
@SerializedName("sort_id")
val sortId: SortId,
@SerializedName("last_conversation_message_id")
val lastConversationMessageId: Int,
@SerializedName("is_marked_unread")
val isMarkedUnread: Boolean,
val last_message_id: Int,
val in_read: Int,
val out_read: Int,
val sort_id: SortId,
val last_conversation_message_id: Int,
val is_marked_unread: Boolean,
val important: Boolean,
@SerializedName("push_settings")
val pushSettings: PushSettings,
@SerializedName("can_write")
val canWrite: CanWrite,
@SerializedName("can_send_money")
val canSendMoney: Boolean,
@SerializedName("can_receive_money")
val canReceiveMoney: Boolean,
@SerializedName("chat_settings")
val chatSettings: ChatSettings?,
@SerializedName("call_in_progress")
val callInProgress: CallInProgress?,
@SerializedName("unread_count")
val unreadCount: Int?
val push_settings: PushSettings,
val can_write: CanWrite,
val can_send_money: Boolean,
val can_receive_money: Boolean,
val chat_settings: ChatSettings?,
val call_in_progress: CallInProgress?,
val unread_count: Int?
) : Parcelable {
fun asVkConversation(lastMessage: VkMessage? = null) = VkConversation(
id = peer.id,
title = chatSettings?.title,
photo200 = chatSettings?.photo?.photo200,
title = chat_settings?.title,
photo200 = chat_settings?.photo?.photo_200,
type = peer.type,
callInProgress = callInProgress != null,
isPhantom = chatSettings?.isDisappearing == true,
lastConversationMessageId = lastConversationMessageId,
inRead = inRead,
outRead = outRead,
isMarkedUnread = isMarkedUnread,
lastMessageId = lastMessageId,
unreadCount = unreadCount,
membersCount = chatSettings?.membersCount,
ownerId = chatSettings?.ownerId,
isPinned = sortId.majorId > 0
callInProgress = call_in_progress != null,
isPhantom = chat_settings?.is_disappearing == true,
lastConversationMessageId = last_conversation_message_id,
inRead = in_read,
outRead = out_read,
isMarkedUnread = is_marked_unread,
lastMessageId = last_message_id,
unreadCount = unread_count,
membersCount = chat_settings?.members_count,
ownerId = chat_settings?.owner_id,
isPinned = sort_id.major_id > 0
).apply {
this.lastMessage = lastMessage
this.pinnedMessage = chatSettings?.pinnedMessage?.asVkMessage()
this.pinnedMessage = chat_settings?.pinned_message?.asVkMessage()
}
@Parcelize
data class Peer(
val id: Int,
val type: String,
@SerializedName("local_id")
val localId: Int
val local_id: Int
) : Parcelable
@Parcelize
data class SortId(
@SerializedName("major_id")
val majorId: Int,
@SerializedName("minor_id")
val minorId: Int
val major_id: Int,
val minor_id: Int
) : Parcelable
@Parcelize
data class PushSettings(
@SerializedName("disabled_forever")
val disabledForever: Boolean,
@SerializedName("no_sound")
val noSound: Boolean,
@SerializedName("disabled_mentions")
val disabledMentions: Boolean,
@SerializedName("disabled_mass_mentions")
val disabledMassMentions: Boolean
val disabled_forever: Boolean,
val no_sound: Boolean,
val disabled_mentions: Boolean,
val disabled_mass_mentions: Boolean
) : Parcelable
@Parcelize
@@ -94,75 +74,50 @@ data class BaseVkConversation(
@Parcelize
data class ChatSettings(
@SerializedName("owner_id")
val ownerId: Int,
val owner_id: Int,
val title: String,
val state: String,
val acl: Acl,
@SerializedName("members_count")
val membersCount: Int,
@SerializedName("friends_count")
val friendsCount: Int,
val members_count: Int,
val friends_count: Int,
val photo: Photo?,
@SerializedName("admin_ids")
val adminsIds: List<Int>,
@SerializedName("active_ids")
val activeIds: List<Int>,
@SerializedName("is_group_channel")
val isGroupChannel: Boolean,
@SerializedName("is_disappearing")
val isDisappearing: Boolean,
@SerializedName("is_service")
val isService: Boolean,
val admin_ids: List<Int>,
val active_ids: List<Int>,
val is_group_channel: Boolean,
val is_disappearing: Boolean,
val is_service: Boolean,
val theme: String?,
@SerializedName("pinned_message")
val pinnedMessage: BaseVkMessage?
val pinned_message: BaseVkMessage?
) : Parcelable {
@Parcelize
data class Acl(
@SerializedName("can_change_info")
val canChangeInfo: Boolean,
@SerializedName("can_change_invite_link")
val canChangeInviteLink: Boolean,
@SerializedName("can_change_pin")
val canChangePin: Boolean,
@SerializedName("can_invite")
val canInvite: Boolean,
@SerializedName("can_promote_users")
val canPromoteUsers: Boolean,
@SerializedName("can_see_invite_link")
val canSeeInviteLink: Boolean,
@SerializedName("can_moderate")
val canModerate: Boolean,
@SerializedName("can_copy_chat")
val canCopyChat: Boolean,
@SerializedName("can_call")
val canCall: Boolean,
@SerializedName("can_use_mass_mentions")
val canUseMassMentions: Boolean,
@SerializedName("can_change_style")
val canChangeStyle: Boolean
val can_change_info: Boolean,
val can_change_invite_link: Boolean,
val can_change_pin: Boolean,
val can_invite: Boolean,
val can_promote_users: Boolean,
val can_see_invite_link: Boolean,
val can_moderate: Boolean,
val can_copy_chat: Boolean,
val can_call: Boolean,
val can_use_mass_mentions: Boolean,
val can_change_style: Boolean
) : Parcelable
@Parcelize
data class Photo(
@SerializedName("photo_50")
val photo50: String?,
@SerializedName("photo_100")
val photo100: String?,
@SerializedName("photo_200")
val photo200: String?,
@SerializedName("is_default_photo")
val isDefaultPhoto: Boolean
val photo_50: String?,
val photo_100: String?,
val photo_200: String?,
val is_default_photo: Boolean
) : Parcelable
}
@Parcelize
data class CallInProgress(
val participants: Participants,
@SerializedName("join_link")
val joinLink: String
val participants: BaseVkGroupCall.Participants,
val join_link: String
) : Parcelable {
@Parcelize
@@ -1,7 +1,6 @@
package com.meloda.fast.api.model.base
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.VkGroup
import kotlinx.parcelize.Parcelize
@@ -9,33 +8,24 @@ import kotlinx.parcelize.Parcelize
data class BaseVkGroup(
val id: Int,
val name: String,
@SerializedName("screen_name")
val screenName: String,
@SerializedName("is_closed")
val isClosed: Int,
val screen_name: String,
val is_closed: Int,
val type: String,
@SerializedName("is_admin")
val isAdmin: Int,
@SerializedName("is_member")
val isMember: Int,
@SerializedName("is_advertiser")
val isAdvertiser: Int,
@SerializedName("photo_50")
val photo50: String?,
@SerializedName("photo_100")
val photo100: String?,
@SerializedName("photo_200")
val photo200: String?,
@SerializedName("members_count")
val membersCount: Int?
val is_admin: Int,
val is_member: Int,
val is_advertiser: Int,
val photo_50: String?,
val photo_100: String?,
val photo_200: String?,
val members_count: Int?
) : Parcelable {
fun asVkGroup() = VkGroup(
id = -id,
name = name,
screenName = screenName,
photo200 = photo200,
membersCount = membersCount
screenName = screen_name,
photo200 = photo_200,
membersCount = members_count
)
}
@@ -1,7 +1,6 @@
package com.meloda.fast.api.model.base
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
@@ -10,23 +9,17 @@ import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkMessage(
val date: Int,
@SerializedName("from_id")
val fromId: Int,
val from_id: Int,
val id: Int,
val out: Int,
@SerializedName("peer_id")
val peerId: Int,
val peer_id: Int,
val text: String,
@SerializedName("conversation_message_id")
val conversationMessageId: Int,
@SerializedName("fwd_messages")
val fwdMessages: List<BaseVkMessage>? = listOf(),
val conversation_message_id: Int,
val fwd_messages: List<BaseVkMessage>? = listOf(),
val important: Boolean,
@SerializedName("random_id")
val randomId: Int,
val random_id: Int,
val attachments: List<BaseVkAttachmentItem> = listOf(),
@SerializedName("is_hidden")
val isHidden: Boolean,
val is_hidden: Boolean,
val payload: String,
val geo: Geo?,
val action: Action?,
@@ -37,20 +30,20 @@ data class BaseVkMessage(
id = id,
text = if (text.isBlank()) null else text,
isOut = out == 1,
peerId = peerId,
fromId = fromId,
peerId = peer_id,
fromId = from_id,
date = date,
randomId = randomId,
randomId = random_id,
action = action?.type,
actionMemberId = action?.memberId,
actionMemberId = action?.member_id,
actionText = action?.text,
actionConversationMessageId = action?.conversationMessageId,
actionConversationMessageId = action?.conversation_message_id,
actionMessage = action?.message,
geoType = geo?.type,
important = important
).also {
it.attachments = VkUtils.parseAttachments(attachments)
it.forwards = VkUtils.parseForwards(fwdMessages)
it.forwards = VkUtils.parseForwards(fwd_messages)
}
@Parcelize
@@ -71,11 +64,9 @@ data class BaseVkMessage(
@Parcelize
data class Action(
val type: String,
@SerializedName("member_id")
val memberId: Int?,
val member_id: Int?,
val text: String?,
@SerializedName("conversation_message_id")
val conversationMessageId: Int?,
val conversation_message_id: Int?,
val message: String?
) : Parcelable
@@ -1,35 +1,24 @@
package com.meloda.fast.api.model.base
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.VkUser
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkUser(
val id: Int,
@SerializedName("first_name")
val firstName: String,
@SerializedName("last_name")
val lastName: String,
@SerializedName("can_access_closed")
val canAccessClosed: Boolean,
@SerializedName("is_closed")
val isClosed: Boolean,
@SerializedName("can_invite_to_chats")
val canInviteToChats: Boolean,
val first_name: String,
val last_name: String,
val can_access_closed: Boolean,
val is_closed: Boolean,
val can_invite_to_chats: Boolean,
val sex: Int?,
@SerializedName("photo_50")
val photo50: String?,
@SerializedName("photo_100")
val photo100: String?,
@SerializedName("photo_200")
val photo200: String?,
val photo_50: String?,
val photo_100: String?,
val photo_200: String?,
val online: Int?,
@SerializedName("online_info")
val onlineInfo: OnlineInfo?,
@SerializedName("screen_name")
val screenName: String
val online_info: OnlineInfo?,
val screen_name: String
//...other fields
) : Parcelable {
@@ -37,24 +26,20 @@ data class BaseVkUser(
data class OnlineInfo(
val visible: Boolean,
val status: String,
@SerializedName("last_seen")
val lastSeen: Int?,
@SerializedName("is_online")
val isOnline: Boolean?,
@SerializedName("online_mobile")
val isOnlineMobile: Boolean?,
@SerializedName("app_id")
val appId: Int?
val last_seen: Int?,
val is_online: Boolean?,
val online_mobile: Boolean?,
val app_id: Int?
) : Parcelable
fun asVkUser() = VkUser(
id = id,
firstName = firstName,
lastName = lastName,
firstName = first_name,
lastName = last_name,
online = online == 1,
photo200 = photo200,
lastSeen = onlineInfo?.lastSeen,
lastSeenStatus = onlineInfo?.status
photo200 = photo_200,
lastSeen = online_info?.last_seen,
lastSeenStatus = online_info?.status
)
}
@@ -1,6 +1,7 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import android.util.Log
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@@ -26,12 +27,16 @@ data class BaseVkAttachmentItem(
val wallReply: BaseVkWallReply?,
val call: BaseVkCall?,
@SerializedName("group_call_in_progress")
val groupCall: BaseVkGroupCall?
val groupCall: BaseVkGroupCall?,
val curator: BaseVkCurator?,
val event: BaseVkEvent?,
val story: BaseVkStory?
) : Parcelable {
fun getPreparedType() = AttachmentType.parse(type)
enum class AttachmentType(val value: String) {
enum class AttachmentType(var value: String) {
UNKNOWN("unknown"),
PHOTO("photo"),
VIDEO("video"),
AUDIO("audio"),
@@ -46,11 +51,22 @@ data class BaseVkAttachmentItem(
POLL("poll"),
WALL_REPLY("wall_reply"),
CALL("call"),
GROUP_CALL_IN_PROGRESS("group_call_in_progress")
GROUP_CALL_IN_PROGRESS("group_call_in_progress"),
CURATOR("curator"),
EVENT("event"),
STORY("story")
;
companion object {
fun parse(value: String) = values().firstOrNull { it.value == value }
fun parse(value: String): AttachmentType? {
val parsedValue = values().firstOrNull { it.value == value } ?: UNKNOWN
if (parsedValue == UNKNOWN) {
Log.e("AttachmentType", "Unknown attachment type: $value")
}
return parsedValue
}
}
}
@@ -1,7 +1,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.VkAudio
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -12,37 +12,33 @@ data class BaseVkAudio(
val duration: Int,
val url: String,
val date: Int,
@SerializedName("owner_id")
val ownerId: Int,
@SerializedName("access_key")
val accessKey: String,
@SerializedName("is_explicit")
val isExplicit: Boolean,
@SerializedName("is_focus_track")
val isFocusTrack: Boolean,
@SerializedName("is_licensed")
val isLicensed: Boolean,
@SerializedName("track_code")
val trackCode: String,
@SerializedName("genre_id")
val genreId: Int,
val owner_id: Int,
val access_key: String,
val is_explicit: Boolean,
val is_focus_track: Boolean,
val is_licensed: Boolean,
val track_code: String,
val genre_id: Int,
val album: Album,
@SerializedName("short_videos_allowed")
val shortVideosAllowed: Boolean,
@SerializedName("stories_allowed")
val storiesAllowed: Boolean,
@SerializedName("stories_cover_allowed")
val storiesCoverAllowed: Boolean
val short_videos_allowed: Boolean,
val stories_allowed: Boolean,
val stories_cover_allowed: Boolean
) : BaseVkAttachment() {
fun asVkAudio() = VkAudio(
id = id,
title = title,
artist = artist,
url = url,
duration = duration
)
@Parcelize
data class Album(
val id: Int,
val title: String,
@SerializedName("owner_id")
val ownerId: Int,
@SerializedName("access_key")
val accessKey: String,
val owner_id: Int,
val access_key: String,
val thumb: Thumb
) : Parcelable {
@@ -50,22 +46,13 @@ data class BaseVkAudio(
data class Thumb(
val width: Int,
val height: Int,
@SerializedName("photo_34")
val photo34: String,
@SerializedName("photo_68")
val photo68: String,
@SerializedName("photo_135")
val photo135: String,
@SerializedName("photo_270")
val photo270: String,
@SerializedName("photo_300")
val photo300: String,
@SerializedName("photo_600")
val photo600: String,
@SerializedName("photo_1200")
val photo1200: String
val photo_34: String,
val photo_68: String,
val photo_135: String,
val photo_270: String,
val photo_300: String,
val photo_600: String,
val photo_1200: String
) : Parcelable
}
}
@@ -1,15 +1,12 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkCall(
@SerializedName("initiator_id")
val initiatorId: Int,
@SerializedName("receiver_id")
val receiverId: Int,
val initiator_id: Int,
val receiver_id: Int,
val state: String,
val time: Int,
val duration: Int,
@@ -0,0 +1,27 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkCurator
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkCurator(
val id: Int,
val name: String,
val description: String,
val url: String,
val photo: List<Photo>
) : BaseVkAttachment() {
fun asVkCurator() = VkCurator(
id = id
)
@Parcelize
data class Photo(
val height: Int,
val url: String,
val width: String
) : Parcelable
}
@@ -0,0 +1,22 @@
package com.meloda.fast.api.model.base.attachments
import com.meloda.fast.api.model.attachments.VkEvent
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkEvent(
val button_text: String,
val id: Int,
val is_favorite: Boolean,
val text: String,
val address: String,
val friends: List<Int> = listOf(),
val member_status: Int,
val time: Int
) : BaseVkAttachment() {
fun asVkEvent() = VkEvent(
id = id
)
}
@@ -1,14 +1,13 @@
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.VkFile
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkFile(
val id: Int,
@SerializedName("owner_id")
val ownerId: Int,
val owner_id: Int,
val title: String,
val size: Int,
val ext: String,
@@ -16,14 +15,19 @@ data class BaseVkFile(
val type: Int,
val url: String,
val preview: Preview?,
@SerializedName("is_licensed")
val isLicensed: Int,
@SerializedName("access_key")
val accessKey: String,
@SerializedName("web_preview_url")
val webPreviewUrl: String?
val ic_licensed: Int,
val access_key: String,
val web_preview_url: String?
) : BaseVkAttachment() {
fun asVkFile() = VkFile(
id = id,
title = title,
ext = ext,
url = url,
size = size
)
@Parcelize
data class Preview(
val photo: Photo?,
@@ -31,15 +35,24 @@ data class BaseVkFile(
) : Parcelable {
@Parcelize
data class Photo(val sizes: List<Size>) : Parcelable
data class Photo(val sizes: List<Size>) : Parcelable {
@Parcelize
data class Size(
val height: Int,
val width: Int,
val type: String,
val src: String
) : Parcelable
}
@Parcelize
data class Video(
val src: String,
val width: Int,
val height: Int,
@SerializedName("file_size")
val fileSize: Int
val file_size: Int
) : Parcelable
}
@@ -1,16 +1,12 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkGift(
val id: Int,
@SerializedName("thumb_256")
val thumb256: String?,
@SerializedName("thumb_96")
val thumb96: String?,
@SerializedName("thumb_48")
val thumb48: String
val thumb_256: String?,
val thumb_96: String?,
val thumb_48: String
) : Parcelable
@@ -1,17 +1,14 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkGraffiti(
val id: Int,
@SerializedName("owner_id")
val ownerId: Int,
val owner_id: Int,
val url: String,
val width: Int,
val height: Int,
@SerializedName("access_key")
val accessKey: String
val access_key: String
) : Parcelable
@@ -1,15 +1,12 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkGroupCall(
@SerializedName("initiator_id")
val initiatorId: Int,
@SerializedName("join_link")
val joinLink: String,
val initiator_id: Int,
val join_link: String,
val participants: Participants
) : Parcelable {
@@ -1,15 +1,25 @@
package com.meloda.fast.api.model.base.attachments
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.attachments.VkLink
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkLink(
val url: String,
val title: String,
val caption: String,
val photo: BaseVkPhoto,
val title: String?,
val caption: String?,
val photo: BaseVkPhoto?,
val target: String,
@SerializedName("is_favorite")
val isFavorite: Boolean
) : BaseVkAttachment()
val is_favorite: Boolean
) : BaseVkAttachment() {
fun asVkLink() = VkLink(
url = url,
title = title,
caption = caption,
photo = photo?.asVkPhoto(),
target = target,
isFavorite = is_favorite
)
}
@@ -10,8 +10,7 @@ data class BaseVkMiniApp(
val description: String,
val app: App,
val images: List<Image>?,
@SerializedName("button_text")
val buttonText: String
val button_text: String
) : Parcelable {
@Parcelize
@@ -1,47 +1,43 @@
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
data class BaseVkPhoto(
@SerializedName("album_id")
val albumId: Int,
val album_id: Int,
val date: Int,
val id: Int,
@SerializedName("owner_id")
val ownerId: Int,
@SerializedName("has_tags")
val hasTags: Boolean,
@SerializedName("access_key")
val accessKey: String?,
val owner_id: Int,
val has_tags: Boolean,
val access_key: String?,
val sizes: List<Size>,
val text: String,
@SerializedName("user_id")
val userId: Int?
val user_id: Int?,
val lat: Double?,
val long: Double?,
val post_id: Int?
) : BaseVkAttachment() {
fun asVkPhoto() = VkPhoto(
albumId = albumId,
albumId = album_id,
date = date,
id = id,
ownerId = ownerId,
hasTags = hasTags,
accessKey = accessKey,
ownerId = owner_id,
hasTags = has_tags,
accessKey = access_key,
sizes = sizes,
text = text,
userId = userId
userId = user_id
)
}
@Parcelize
data class Size(
val height: Int,
val width: Int,
val type: String,
val url: String
) : Parcelable
@Parcelize
data class Size(
val height: Int,
val width: Int,
val type: String,
@SerializedName("url", alternate = ["src"])
val url: String,
) : Parcelable
}
@@ -1,7 +1,6 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
@@ -11,30 +10,20 @@ data class BaseVkPoll(
val votes: Int,
val anonymous: Boolean,
val closed: Boolean,
@SerializedName("end_date")
val endDate: Int,
@SerializedName("is_board")
val isBoard: Boolean,
@SerializedName("can_vote")
val canVote: Boolean,
@SerializedName("can_edit")
val canEdit: Boolean,
@SerializedName("can_report")
val canReport: Boolean,
@SerializedName("can_share")
val canShare: Boolean,
val end_date: Int,
val is_board: Boolean,
val can_vote: Boolean,
val can_edit: Boolean,
val can_report: Boolean,
val can_share: Boolean,
val created: Int,
@SerializedName("owner_id")
val ownerId: Int,
val owner_id: Int,
val question: String,
@SerializedName("disable_unvote")
val disableUnVote: Boolean,
val disable_unvote: Boolean,
val friends: List<Friend>?,
@SerializedName("embed_hash")
val embedHash: String,
val embed_hash: String,
val answers: List<Answer>,
@SerializedName("author_id")
val authorId: Int,
val author_id: Int,
val background: Background?
) : Parcelable {
@@ -1,30 +1,24 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import androidx.annotation.IntDef
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.attachments.VkSticker
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkSticker(
@SerializedName("product_id")
val productId: Int,
@SerializedName("sticker_id")
val stickerId: Int,
val product_id: Int,
val sticker_id: Int,
val images: List<Image>,
@SerializedName("images_with_background")
val imagesWithBackground: List<Image>,
@SerializedName("animation_url")
val animationUrl: String?,
val images_with_background: List<Image>,
val animation_url: String?,
val animations: List<Animation>?
) : Parcelable {
fun asVkSticker() = VkSticker(
id = stickerId,
productId = productId,
id = sticker_id,
productId = product_id,
images = images,
backgroundImages = imagesWithBackground
backgroundImages = images_with_background
)
@Parcelize
@@ -41,6 +35,3 @@ data class BaseVkSticker(
) : Parcelable
}
@IntDef(64, 128, 256, 352)
annotation class StickerSize
@@ -0,0 +1,52 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.meloda.fast.api.model.attachments.VkStory
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkStory(
val id: Int,
val owner_id: Int,
val access_key: String,
val can_comment: Int,
val can_reply: Int,
val can_like: Boolean,
val can_share: Int,
val can_hide: Int,
val date: Int,
val expires_at: Int,
val is_ads: Boolean,
val photo: BaseVkPhoto,
val replies: Replies,
val is_one_time: Boolean,
val track_code: String,
val type: String,
val views: Int,
val likes_count: Int,
val reaction_set_id: String,
val is_restricted: Boolean,
val no_sound: Boolean,
val need_mute: Boolean,
val mute_reply: Boolean,
val can_ask: Int,
val can_ask_anonymous: Int,
val preloading_enabled: Boolean,
val narratives_count: Int,
val can_use_in_narrative: Boolean
) : BaseVkAttachment() {
fun asVkStory() = VkStory(
id = id,
ownerId = owner_id,
date = date,
photo = photo.asVkPhoto()
)
@Parcelize
data class Replies(
val count: Int,
val new: Int
) : Parcelable
}
@@ -1,11 +1,9 @@
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,
@@ -20,45 +18,30 @@ data class BaseVkVideo(
val added: Int,
val type: String,
val views: Int,
@SerializedName("can_comment")
val canComment: Int,
@SerializedName("can_edit")
val canEdit: Int,
@SerializedName("can_like")
val canLike: Int,
@SerializedName("can_repost")
val canRepost: Int,
@SerializedName("can_subscribe")
val canSubscribe: Int,
@SerializedName("can_add_to_faves")
val canAddToFaves: Int,
@SerializedName("can_add")
val canAdd: Int,
@SerializedName("can_attach_link")
val canAttachLink: Int,
@SerializedName("access_key")
val accessKey: String,
@SerializedName("owner_id")
val ownerId: Int,
@SerializedName("ov_id")
val ovId: String,
@SerializedName("is_favorite")
val isFavorite: Boolean,
@SerializedName("track_code")
val trackCode: String,
val can_comment: Int,
val can_edit: Int,
val can_like: Int,
val can_repost: Int,
val can_subscribe: Int,
val can_add_to_faves: Int,
val can_add: Int,
val can_attach_link: Int,
val access_key: String,
val owner_id: Int,
val ov_id: String,
val is_favorite: Boolean,
val track_code: String,
val image: List<Image>,
@SerializedName("first_frame")
val firstFrame: List<FirstFrame>,
val first_frame: List<FirstFrame>,
val files: File,
@SerializedName("timeline_thumbs")
val timelineThumbs: TimelineThumbs
//ads
val timeline_thumbs: TimelineThumbs,
val ads: Ads
) : BaseVkAttachment() {
fun asVkVideo() = VkVideo(
id = id,
images = image,
firstFrames = firstFrame
firstFrames = first_frame
)
@Parcelize
@@ -66,8 +49,7 @@ data class BaseVkVideo(
val height: Int,
val width: Int,
val url: String,
@SerializedName("with_padding")
val withPadding: Int
val with_padding: Int
) : Parcelable
@Parcelize
@@ -86,34 +68,62 @@ data class BaseVkVideo(
val mp4_1080: String?,
val mp4_1440: String?,
val hls: String,
@SerializedName("dash_uni")
val dashUni: String,
@SerializedName("dash_sep")
val dashSep: String,
@SerializedName("hls_ondemand")
val hlsOnDemand: String,
@SerializedName("dash_ondemand")
val dashOnDemand: String,
@SerializedName("failover_host")
val failOverHost: String
val dash_uni: String,
val dash_sep: String,
val hls_ondemand: String,
val dash_ondemand: String,
val failover_host: String
) : Parcelable
@Parcelize
data class TimelineThumbs(
@SerializedName("count_per_image")
val countPerImage: Int,
@SerializedName("count_per_row")
val countPerRow: Int,
@SerializedName("count_total")
val countTotal: Int,
@SerializedName("frame_height")
val frameHeight: Int,
@SerializedName("frame_width")
val frameWidth: Float,
val count_per_image: Int,
val count_per_row: Int,
val count_total: Int,
val frame_height: Int,
val frame_width: Float,
val links: List<String>,
@SerializedName("is_uv")
val isUv: Boolean,
val is_uv: Boolean,
val frequency: Int
) : Parcelable
@Parcelize
data class Ads(
val slot_id: Int,
val timeout: Int,
val can_play: Int,
val params: Params,
val sections: List<String>,
val midroll_percents: List<Float>
) : Parcelable {
@Parcelize
data class Params(
val vk_id: Int,
val duration: Int,
val video_id: String,
val pl: Int,
val content_id: String,
val lang: Int,
val puid1: String,
val puid2: Int,
val puid3: Int,
val puid5: Int,
val puid6: Int,
val puid7: Int,
val puid9: Int,
val puid10: Int,
val puid12: Int,
val puid13: Int,
val puid14: Int,
val puid15: Int,
val puid18: Int,
val puid21: Int,
val sign: String,
val groupId: Int,
val vk_catid: Int,
val is_xz_video: Int
) : Parcelable
}
}
@@ -1,23 +1,17 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkVoiceMessage(
val id: Int,
@SerializedName("owner_id")
val ownerId: Int,
val owner_id: Int,
val duration: Int,
val waveform: List<Int>,
@SerializedName("link_ogg")
val linkOgg: String,
@SerializedName("link_mp3")
val linkMp3: String,
@SerializedName("access_key")
val accessKey: String,
@SerializedName("transcript_state")
val transcriptState: String,
val link_ogg: String,
val link_mp3: String,
val access_key: String,
val transcript_state: String,
val transcript: String
) : Parcelable
@@ -1,34 +1,43 @@
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.VkWall
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkWall(
val id: Int,
@SerializedName("from_id")
val fromId: Int,
@SerializedName("to_id")
val toId: Int,
val from_id: Int,
val to_id: Int,
val date: Int,
val text: String,
val attachments: List<BaseVkAttachmentItem>?,
@SerializedName("post_source")
val postSource: PostSource,
val post_source: PostSource,
val comments: Comments,
val likes: Likes,
val reposts: Reposts,
val views: Views,
@SerializedName("is_favorite")
val isFavorite: Boolean,
val is_favorite: Boolean,
val donut: Donut,
@SerializedName("access_key")
val accessKey: String,
@SerializedName("short_text_rate")
val shortTextRate: Double
val access_key: String,
val short_text_rate: Double
) : Parcelable {
fun asVkWall() = VkWall(
id = id,
fromId = from_id,
toId = to_id,
date = date,
text = text,
attachments = attachments,
comments = comments.count,
likes = likes.count,
reposts = reposts.count,
views = views.count,
isFavorite = is_favorite,
accessKey = access_key
)
@Parcelize
data class PostSource(
val type: String,
@@ -38,28 +47,22 @@ data class BaseVkWall(
@Parcelize
data class Comments(
val count: Int,
@SerializedName("can_post")
val canPost: Int,
@SerializedName("groups_can_post")
val groupsCanPost: Boolean
val can_post: Int,
val groups_can_post: Boolean
) : Parcelable
@Parcelize
data class Likes(
val count: Int,
@SerializedName("user_likes")
val userLikes: Int,
@SerializedName("can_like")
val canLike: Int,
@SerializedName("can_publish")
val canPublish: Int,
val user_likes: Int,
val can_like: Int,
val can_publish: Int,
) : Parcelable
@Parcelize
data class Reposts(
val count: Int,
@SerializedName("user_reposted")
val userReposted: Int
val user_reposted: Int
) : Parcelable
@Parcelize
@@ -69,8 +72,7 @@ data class BaseVkWall(
@Parcelize
data class Donut(
@SerializedName("is_donut")
val isDonut: Boolean
val is_donut: Boolean
) : Parcelable
}
@@ -1,39 +1,29 @@
package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize
@Parcelize
data class BaseVkWallReply(
val id: Int,
@SerializedName("from_id")
val fromId: Int,
val from_id: Int,
val date: Int,
val text: String,
@SerializedName("post_id")
val postId: Int,
@SerializedName("owner_id")
val ownerId: Int,
@SerializedName("parents_stack")
val parentsStack: List<Int>,
val post_id: Int,
val owner_id: Int,
val parents_stack: List<Int>,
val likes: Likes,
@SerializedName("reply_to_user")
val replyToUser: Int?,
@SerializedName("reply_to_comment")
val replyToComment: Int?
val reply_to_user: Int?,
val reply_to_comment: Int?
) : Parcelable {
@Parcelize
data class Likes(
val count: Int,
@SerializedName("can_like")
val canLike: Int,
@SerializedName("user_likes")
val userLikes: Int,
@SerializedName("can_publish")
val canPublish: Int
val can_like: Int,
val user_likes: Int,
val can_publish: Int
) : Parcelable
}
@@ -77,18 +77,21 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
) : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
var isVkException = true
val result: Answer<T> =
if (response.isSuccessful) {
val baseBody = response.body()
if (baseBody !is ApiResponse<*>) Answer.Success(baseBody as T)
else {
val body = baseBody as ApiResponse<*>
if (body.error != null) Answer.Error(body.error)
else Answer.Success(body as T)
if (body.error != null) {
Answer.Error(body.error)
} else Answer.Success(body as T)
}
} else Answer.Error(IOException(response.errorBody()?.string() ?: ""))
if (result is Answer.Error) if (checkErrors(call, result)) return
if (result is Answer.Error && isVkException) if (checkErrors(call, result)) return
callback.onResponse(proxy, Response.success(result))
@@ -7,15 +7,16 @@ import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.Resources
import android.net.ConnectivityManager
import android.util.Log
import android.view.inputmethod.InputMethodManager
import androidx.core.content.pm.PackageInfoCompat
import androidx.preference.PreferenceManager
import androidx.room.Room
import com.meloda.fast.BuildConfig
import com.meloda.fast.database.AppDatabase
import com.meloda.fast.util.AndroidUtils
import dagger.hilt.android.HiltAndroidApp
import org.acra.ACRA
import kotlin.math.sqrt
@HiltAndroidApp
class AppGlobal : Application() {
@@ -66,8 +67,24 @@ class AppGlobal : Application() {
Companion.packageName = packageName
Companion.packageManager = packageManager
screenWidth = AndroidUtils.getDisplayWidth()
screenHeight = AndroidUtils.getDisplayHeight()
screenWidth = resources.displayMetrics.widthPixels
screenHeight = resources.displayMetrics.heightPixels
val density = resources.displayMetrics.density
val densityDpi = resources.displayMetrics.densityDpi
val densityScaled = resources.displayMetrics.scaledDensity
val xDpi = resources.displayMetrics.xdpi
val yDpi = resources.displayMetrics.ydpi
val diagonal = sqrt(
(screenWidth * screenWidth - screenHeight * screenHeight).toFloat()
)
Log.i(
"Fast::DeviceInfo",
"width: $screenWidth; height: $screenHeight; density: $density; diagonal: $diagonal; dpiDensity: $densityDpi; scaledDensity: $densityScaled; xDpi: $xDpi; yDpi: $yDpi"
)
inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@@ -0,0 +1,23 @@
package com.meloda.fast.common
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
object AppSettings {
val keyIsMultilineEnabled = booleanPreferencesKey("isMultilineEnabled")
}
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
name = "settings",
corruptionHandler = null,
scope = CoroutineScope(Dispatchers.IO + Job())
)
@@ -4,6 +4,7 @@ import android.content.Context
import android.content.res.ColorStateList
import android.graphics.drawable.ColorDrawable
import android.text.SpannableString
import android.text.TextUtils
import android.text.style.ForegroundColorSpan
import android.view.ViewGroup
import androidx.core.content.ContextCompat
@@ -27,7 +28,8 @@ class ConversationsAdapter constructor(
context: Context,
values: MutableList<VkConversation>,
val profiles: HashMap<Int, VkUser> = hashMapOf(),
val groups: HashMap<Int, VkGroup> = hashMapOf()
val groups: HashMap<Int, VkGroup> = hashMapOf(),
var isMultilineEnabled: Boolean = true
) : BaseAdapter<VkConversation, ConversationsAdapter.ItemHolder>(
context, values, COMPARATOR
) {
@@ -41,6 +43,11 @@ class ConversationsAdapter constructor(
private val dateColor = ContextCompat.getColor(context, R.color.n2_500)
private val youPrefix = context.getString(R.string.you_message_prefix)
init {
binding.title.ellipsize = TextUtils.TruncateAt.END
binding.message.ellipsize = TextUtils.TruncateAt.END
}
override fun bind(position: Int) {
val conversation = getItem(position)
@@ -48,6 +55,11 @@ class ConversationsAdapter constructor(
binding.callIcon.isVisible = conversation.callInProgress
binding.phantomIcon.isVisible = conversation.isPhantom
val maxLines = if (isMultilineEnabled) 2 else 1
binding.title.maxLines = maxLines
binding.message.maxLines = maxLines
val message = if (conversation.lastMessage != null) conversation.lastMessage!!
else {
binding.title.text = conversation.title
@@ -129,6 +141,7 @@ class ConversationsAdapter constructor(
binding.pin.isVisible = conversation.isPinned
val actionMessage = VkUtils.getActionConversationText(
context = context,
message = message,
youPrefix = youPrefix,
profiles = profiles,
@@ -6,10 +6,13 @@ import android.viewbinding.library.fragment.viewBinding
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.datastore.preferences.core.edit
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
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
@@ -17,9 +20,14 @@ 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.common.AppSettings
import com.meloda.fast.common.dataStore
import com.meloda.fast.databinding.FragmentConversationsBinding
import com.meloda.fast.util.AndroidUtils
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlin.math.abs
@AndroidEntryPoint
@@ -59,7 +67,18 @@ class ConversationsFragment :
binding.recyclerView.adapter = adapter
binding.createChat.setOnClickListener {}
lifecycleScope.launch {
requireContext().dataStore.data.map {
adapter.isMultilineEnabled = it[AppSettings.keyIsMultilineEnabled] ?: true
adapter.notifyItemRangeChanged(0, adapter.itemCount)
}.collect { }
}
binding.createChat.setOnClickListener {
Snackbar.make(it, "Test snackbar", Snackbar.LENGTH_SHORT)
.setAction("Action") {}
.show()
}
UserConfig.vkUser.observe(viewLifecycleOwner) {
it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } }
@@ -87,6 +106,18 @@ class ConversationsFragment :
viewModel.loadProfileUser()
viewModel.loadConversations()
binding.avatar.setOnClickListener {
lifecycleScope.launch {
requireContext().dataStore.edit { settings ->
val isMultilineEnabled = settings[AppSettings.keyIsMultilineEnabled] ?: true
settings[AppSettings.keyIsMultilineEnabled] = !isMultilineEnabled
adapter.isMultilineEnabled = !isMultilineEnabled
adapter.notifyItemRangeChanged(0, adapter.itemCount)
}
}
}
}
override fun onEvent(event: VKEvent) {
@@ -0,0 +1,309 @@
package com.meloda.fast.screens.messages
import android.content.Context
import android.content.res.ColorStateList
import android.util.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.Space
import androidx.appcompat.widget.AppCompatImageView
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isNotEmpty
import androidx.core.view.isVisible
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.*
import com.meloda.fast.databinding.*
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.widget.RoundedFrameLayout
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.roundToInt
class AttachmentInflater constructor(
private val context: Context,
private val container: LinearLayoutCompat,
private val message: VkMessage,
private val profiles: Map<Int, VkUser>,
private val groups: Map<Int, VkGroup>
) {
private lateinit var attachments: List<VkAttachment>
private val inflater = LayoutInflater.from(context)
private val playColor = ContextCompat.getColor(context, R.color.a3_700)
private val playBackgroundColor = ContextCompat.getColor(context, R.color.a3_200)
fun inflate() {
if (message.attachments.isNullOrEmpty()) return
attachments = message.attachments!!
container.removeAllViews()
if (attachments.size == 1) {
when (val attachment = attachments[0]) {
is VkSticker -> return sticker(attachment)
is VkWall -> return wall(attachment)
}
}
if (attachments.size > 1) {
if (VkUtils.isAttachmentsHaveOneType(attachments) && attachments[0] is VkPhoto) {
return attachments.forEach { photo(it as VkPhoto) }
}
if (VkUtils.isAttachmentsHaveOneType(attachments) && attachments[0] is VkVideo) {
return attachments.forEach { video(it as VkVideo) }
}
if (VkUtils.isAttachmentsHaveOneType(attachments) && attachments[0] is VkAudio) {
return attachments.forEach { audio(it as VkAudio) }
}
if (VkUtils.isAttachmentsHaveOneType(attachments) && attachments[0] is VkFile) {
return attachments.forEach { file(it as VkFile) }
}
}
attachments.forEach { attachment ->
when (attachment) {
is VkPhoto -> photo(attachment)
is VkVideo -> video(attachment)
is VkAudio -> audio(attachment)
is VkFile -> file(attachment)
is VkLink -> link(attachment)
is VkStory -> story(attachment)
else -> Log.e(
"Attachment inflater",
"Unknown attachment type: ${attachment.javaClass.name}"
)
}
}
}
private fun photo(photo: VkPhoto) {
val size = photo.sizeOfType('m') ?: return
val newPhoto = ShapeableImageView(context).apply {
layoutParams = LinearLayoutCompat.LayoutParams(
AndroidUtils.px(size.width).roundToInt(),
AndroidUtils.px(size.height).roundToInt()
)
shapeAppearanceModel =
shapeAppearanceModel.withCornerSize {
AndroidUtils.px(5)
}
scaleType = ImageView.ScaleType.CENTER_CROP
}
val spacer = Space(context).also {
it.layoutParams = LinearLayoutCompat.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
AndroidUtils.px(5).roundToInt()
)
}
if (container.isNotEmpty())
container.addView(spacer)
if (attachments.size == 1) {
val roundedLayout = RoundedFrameLayout(context).apply {
setTopRightCornerRadius((if (message.isOut) 30 else 40).toFloat())
setTopLeftCornerRadius((if (message.isOut) 40 else 30).toFloat())
setBottomRightCornerRadius((if (message.isOut) 5 else 40).toFloat())
setBottomLeftCornerRadius((if (message.isOut) 40 else 5).toFloat())
}
roundedLayout.addView(newPhoto)
container.addView(roundedLayout)
} else {
container.addView(newPhoto)
}
newPhoto.load(size.url) { crossfade(100) }
}
private fun video(video: VkVideo) {
val size = video.images[1]
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 (container.isNotEmpty())
container.addView(spacer)
container.addView(layout)
newPhoto.load(size.url) { crossfade(100) }
}
private fun audio(audio: VkAudio) {
val binding = ItemMessageAttachmentAudioBinding.inflate(inflater, container, true)
binding.title.text = audio.title
binding.artist.text = "%s | %s".format(
audio.artist,
SimpleDateFormat("mm:ss", Locale.getDefault()).format(audio.duration * 1000L)
)
}
private fun file(file: VkFile) {
val binding = ItemMessageAttachmentFileBinding.inflate(inflater, container, true)
binding.title.text = file.title
binding.size.text = "%s | %s".format(
AndroidUtils.bytesToHumanReadableSize(file.size.toDouble()),
file.ext.uppercase()
)
}
private fun link(link: VkLink) {
val binding = ItemMessageAttachmentLinkBinding.inflate(inflater, container, true)
binding.title.text = link.title
binding.title.isVisible = !link.title.isNullOrBlank()
binding.caption.text = link.caption
binding.caption.isVisible = !link.caption.isNullOrBlank()
binding.preview.shapeAppearanceModel.toBuilder()
.setAllCornerSizes(40f)
.build()
.let {
binding.preview.shapeAppearanceModel = it
}
link.photo?.sizeOfType('m')?.let {
binding.preview.load(it.url) { crossfade(150) }
binding.preview.isVisible = true
return
}
binding.preview.isVisible = false
}
private fun sticker(sticker: VkSticker) {
val binding = ItemMessageAttachmentStickerBinding.inflate(inflater, container, true)
val url = sticker.urlForSize(352)
with(binding.image) {
layoutParams = LinearLayoutCompat.LayoutParams(
AndroidUtils.px(180).roundToInt(),
AndroidUtils.px(180).roundToInt()
)
load(url) { crossfade(150) }
}
}
private fun wall(wall: VkWall) {
val binding = ItemMessageAttachmentWallPostBinding.inflate(inflater, container, true)
val group = if (wall.fromId > 0) null else groups[wall.fromId]
val user = if (wall.fromId < 0) null else profiles[wall.fromId]
val postTitleRes = when {
group != null && user == null -> R.string.post_type_community
user != null && group == null -> R.string.post_type_user
else -> R.string.post_type_unknown
}
val avatar = when {
group == null && user != null -> user.photo200
user == null && group != null -> group.photo200
else -> null
}
val title = when {
group == null && user != null -> user.fullName
user == null && group != null -> group.name
else -> "..."
}
binding.postTitle.text = context.getString(postTitleRes)
binding.postTitle.isVisible = false
binding.avatar.isVisible = group != null || user != null
binding.avatar.shapeAppearanceModel.toBuilder()
.setAllCornerSizes(40f)
.build()
.let {
binding.avatar.shapeAppearanceModel = it
}
if (binding.avatar.isVisible) {
binding.avatar.load(avatar) { crossfade(150) }
} else {
binding.avatar.setImageDrawable(null)
}
binding.title.text = title
binding.date.text = SimpleDateFormat(
"dd.MM.yyyy HH:mm",
Locale.getDefault()
).format(wall.date * 1000L)
}
private fun story(story: VkStory) {
}
}
@@ -3,12 +3,9 @@ package com.meloda.fast.screens.messages
import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
import coil.load
@@ -19,13 +16,10 @@ 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.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
@@ -45,21 +39,6 @@ class MessagesHistoryAdapter constructor(
getItem(position).let { message ->
if (message.action != null) return SERVICE
if (!message.attachments.isNullOrEmpty()) {
val attachments = message.attachments ?: return@let
if (VkUtils.isAttachmentsHaveOneType(attachments) &&
attachments[0] is VkPhoto
) 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
}
if (message.isOut) return OUTGOING
if (!message.isOut) return INCOMING
}
@@ -72,26 +51,12 @@ class MessagesHistoryAdapter constructor(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
return when (viewType) {
// magick numbers is great!
HEADER -> Header(createEmptyView(60))
FOOTER -> Footer(createEmptyView(36))
SERVICE -> ServiceMessage(
ItemMessageServiceBinding.inflate(inflater, parent, false)
)
ATTACHMENT_STICKER_IN -> AttachmentStickerIncoming(
ItemMessageAttachmentStickerInBinding.inflate(inflater, parent, false)
)
ATTACHMENT_STICKER_OUT -> AttachmentStickerOutgoing(
ItemMessageAttachmentStickerOutBinding.inflate(inflater, parent, false)
)
ATTACHMENT_PHOTOS_IN -> AttachmentPhotosIncoming(
ItemMessageAttachmentPhotosInBinding.inflate(inflater, parent, false)
)
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)
)
@@ -130,81 +95,31 @@ class MessagesHistoryAdapter constructor(
private val binding: ItemMessageInBinding
) : Holder(binding.root) {
private val backgroundNormal =
ContextCompat.getDrawable(context, R.drawable.ic_message_in_background)
private val backgroundMiddle =
ContextCompat.getDrawable(context, R.drawable.ic_message_in_background_middle)
init {
MessagesManager.setRootMaxWidth(binding.bubble)
}
override fun bind(position: Int) {
val message = getItem(position)
val prevMessage = getOrNull(position - 1)
val nextMessage = getOrNull(position + 1)
binding.unread.isVisible = message.isRead(conversation)
binding.bubble.background =
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormal
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddle
else backgroundNormal
if (!message.isPeerChat()) {
binding.title.isVisible = false
binding.avatar.isVisible = false
binding.spacer.isVisible =
!(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60)
} else {
binding.title.isVisible =
if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat()
else message.date - prevMessage.date >= 60
binding.spacer.isVisible = binding.title.isVisible
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
}
val messageUser: VkUser? = if (message.isUser()) {
profiles[message.fromId]
} else null
val messageGroup: VkGroup? = if (message.isGroup()) {
groups[message.fromId]
} else null
MessagesManager.loadMessageAvatar(
MessagesPreparator(
context = context,
conversation = conversation,
message = message,
messageUser = messageUser,
messageGroup = messageGroup,
imageView = binding.avatar
)
prevMessage = prevMessage,
nextMessage = nextMessage,
val title = when {
message.isUser() && messageUser != null -> messageUser.firstName
message.isGroup() && messageGroup != null -> messageGroup.name
else -> null
}
avatar = binding.avatar,
bubble = binding.bubble,
text = binding.text,
spacer = binding.spacer,
time = binding.time,
unread = binding.unread,
attachmentContainer = binding.attachmentContainer,
attachmentSpacer = binding.attachmentSpacer,
binding.title.text = title
binding.title.measure(0, 0)
if (binding.title.isVisible) {
binding.bubble.minimumWidth = binding.title.measuredWidth + 60
} else {
binding.bubble.minimumWidth = 0
}
MessagesManager.setMessageText(
message = message,
textView = binding.text
)
profiles = profiles,
groups = groups
).prepare()
}
}
@@ -212,23 +127,8 @@ class MessagesHistoryAdapter constructor(
private val binding: ItemMessageOutBinding
) : Holder(binding.root) {
private val backgroundNormal =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background)
private val backgroundMiddle =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle)
private val backgroundStroke =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke)
private val backgroundMiddleStroke =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke)
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) {
@@ -236,27 +136,25 @@ class MessagesHistoryAdapter constructor(
val prevMessage = getOrNull(position - 1)
binding.text.text = message.text ?: "[no_message]"
MessagesPreparator(
context = context,
conversation = conversation,
message = message,
prevMessage = prevMessage,
binding.unread.isVisible = message.isRead(conversation)
bubble = binding.bubble,
bubbleStroke = binding.bubbleStroke,
text = binding.text,
spacer = binding.spacer,
time = binding.time,
unread = binding.unread,
attachmentContainer = binding.attachmentContainer,
attachmentSpacer = binding.attachmentSpacer,
binding.spacer.isVisible =
!(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60)
binding.bubble.background =
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormal
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddle
else backgroundNormal
binding.bubbleStroke.background =
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)
profiles = profiles,
groups = groups
).prepare()
}
}
inner class ServiceMessage(
@@ -285,6 +183,7 @@ class MessagesHistoryAdapter constructor(
message.action ?: return
binding.message.text = VkUtils.getActionMessageText(
context = context,
message = message,
youPrefix = youPrefix,
profiles = profiles,
@@ -314,233 +213,6 @@ class MessagesHistoryAdapter constructor(
}
}
inner class AttachmentPhotosIncoming(
private val binding: ItemMessageAttachmentPhotosInBinding
) : 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.loadPhotos(
context = context,
message = message,
photosContainer = binding.photosContainer
)
MessagesManager.setMessageText(
message = message,
textView = binding.text
)
binding.bubble.isVisible = binding.text.text.toString().isNotEmpty()
}
}
inner class AttachmentPhotosOutgoing(
private val binding: ItemMessageAttachmentPhotosOutBinding
) : Holder(binding.root) {
override fun bind(position: Int) {
val message = getItem(position)
MessagesManager.loadPhotos(
context = context,
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) {
override fun bind(position: Int) {
val message = getItem(position)
val prevMessage = getOrNull(position - 1)
val nextMessage = getOrNull(position + 1)
if (!message.isPeerChat()) {
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
}
val sticker = message.attachments?.get(0) as? VkSticker ?: return
val url = sticker.urlForSize(352)!!
binding.photo.layoutParams.also {
it.width = 352
it.height = 352
}
binding.photo.load(url) { crossfade(150) }
}
}
inner class AttachmentStickerIncoming(
private val binding: ItemMessageAttachmentStickerInBinding
) : Holder(binding.root) {
override fun bind(position: Int) {
val message = getItem(position)
val prevMessage = getOrNull(position - 1)
val nextMessage = getOrNull(position + 1)
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
}
val messageUser: VkUser? = if (message.isUser()) {
profiles[message.fromId]
} else null
val messageGroup: VkGroup? = if (message.isGroup()) {
groups[message.fromId]
} else null
val avatar = when {
message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200
message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200
else -> null
}
binding.avatar.load(avatar) { crossfade(100) }
val title = when {
message.isUser() && messageUser != null -> messageUser.fullName
message.isGroup() && messageGroup != null -> messageGroup.name
else -> null
}
binding.avatar.setOnLongClickListener {
Toast.makeText(context, title, Toast.LENGTH_SHORT).apply {
setGravity(
Gravity.START or Gravity.BOTTOM,
0,
-50
)
}.show()
true
}
val sticker = message.attachments?.get(0) as? VkSticker ?: return
val url = sticker.urlForSize(352)!!
binding.photo.layoutParams.also {
it.width = 352
it.height = 352
}
binding.photo.load(url) { crossfade(150) }
}
}
private val actualSize get() = values.size
override fun getItemCount(): Int {
@@ -556,13 +228,6 @@ class MessagesHistoryAdapter constructor(
private const val OUTGOING = 4
private const val ATTACHMENT_PHOTOS_IN = 101
private const val ATTACHMENT_PHOTOS_OUT = 102
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(
oldItem: VkMessage,
@@ -3,6 +3,7 @@ package com.meloda.fast.screens.messages
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.viewbinding.library.fragment.viewBinding
import androidx.core.view.isVisible
@@ -76,6 +77,9 @@ class MessagesHistoryFragment :
else -> null
}
binding.title.ellipsize = TextUtils.TruncateAt.END
binding.status.ellipsize = TextUtils.TruncateAt.END
binding.title.text = title ?: "..."
val status = when {
@@ -64,7 +64,7 @@ class MessagesHistoryViewModel @Inject constructor(
response.conversations?.let { baseConversations ->
baseConversations.forEach { baseConversation ->
baseConversation.asVkConversation(
messages[baseConversation.lastMessageId]
messages[baseConversation.last_message_id]
).let { conversation -> conversations[conversation.id] = conversation }
}
}
@@ -1,179 +0,0 @@
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
import com.meloda.fast.widget.BoundedLinearLayout
import kotlin.math.roundToInt
object MessagesManager {
fun setRootMaxWidth(
layout: View
) {
val maxWidth = (AppGlobal.screenWidth * 0.7).roundToInt()
if (layout is BoundedFrameLayout) {
layout.maxWidth = maxWidth
} else if (layout is BoundedLinearLayout) {
layout.maxWidth = maxWidth
}
}
fun loadPhotos(
context: Context,
message: VkMessage,
photosContainer: LinearLayoutCompat
) {
photosContainer.removeAllViews()
message.attachments?.let { attachments ->
val photos = attachments.map { it as VkPhoto }
photos.forEach { photo ->
val size = photo.sizeOfType('m') ?: return
val newPhoto = ShapeableImageView(context).also {
it.layoutParams = LinearLayoutCompat.LayoutParams(
AndroidUtils.px(size.width).roundToInt(),
AndroidUtils.px(size.height).roundToInt()
)
it.shapeAppearanceModel =
it.shapeAppearanceModel.withCornerSize { AndroidUtils.px(5) }
it.scaleType = ImageView.ScaleType.CENTER_CROP
}
val spacer = Space(context).also {
it.layoutParams = LinearLayoutCompat.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
AndroidUtils.px(5).roundToInt()
)
}
if (photosContainer.isNotEmpty())
photosContainer.addView(spacer)
photosContainer.addView(newPhoto)
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) }
}
}
}
fun loadMessageAvatar(
message: VkMessage,
messageUser: VkUser?,
messageGroup: VkGroup?,
imageView: ImageView
) {
val avatar = when {
message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200
message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200
else -> null
}
imageView.load(avatar) { crossfade(100) }
}
fun setMessageText(
message: VkMessage,
textView: TextView
) {
textView.text = VkUtils.prepareMessageText(message.text)
}
}
@@ -0,0 +1,206 @@
package com.meloda.fast.screens.messages
import android.content.Context
import android.util.Log
import android.view.View
import android.widget.ImageView
import android.widget.Space
import android.widget.TextView
import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.core.view.setPadding
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.VkSticker
import com.meloda.fast.common.AppGlobal
import com.meloda.fast.util.AndroidUtils
import com.meloda.fast.widget.BoundedLinearLayout
import java.text.SimpleDateFormat
import java.util.*
import kotlin.math.roundToInt
class MessagesPreparator constructor(
private val context: Context,
private val conversation: VkConversation,
private val message: VkMessage,
private val prevMessage: VkMessage? = null,
private val nextMessage: VkMessage? = null,
private val bubble: BoundedLinearLayout? = null,
private val bubbleStroke: View? = null,
private val text: TextView? = null,
private val avatar: ImageView? = null,
private val title: TextView? = null,
private val spacer: Space? = null,
private val unread: ImageView? = null,
private val time: TextView? = null,
private val attachmentContainer: LinearLayoutCompat? = null,
private val attachmentSpacer: Space? = null,
private val profiles: Map<Int, VkUser>,
private val groups: Map<Int, VkGroup>
) {
init {
val maxWidth = (AppGlobal.screenWidth * 0.7).roundToInt()
if (bubble != null) bubble.maxWidth = maxWidth
}
private val backgroundNormalIn =
ContextCompat.getDrawable(context, R.drawable.ic_message_in_background)
private val backgroundMiddleIn =
ContextCompat.getDrawable(context, R.drawable.ic_message_in_background_middle)
private val backgroundNormalOut =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background)
private val backgroundMiddleOut =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle)
private val backgroundStrokeOut =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke)
private val backgroundMiddleStrokeOut =
ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke)
fun prepare() {
val messageUser: VkUser? = if (message.isUser()) {
profiles[message.fromId]
} else null
val messageGroup: VkGroup? = if (message.isGroup()) {
groups[message.fromId]
} else null
if (unread != null) {
unread.isVisible = message.isRead(conversation)
}
if (bubble != null && time != null) {
bubble.setOnClickListener { time.isVisible = !time.isVisible }
}
if (attachmentContainer != null) {
if (message.attachments.isNullOrEmpty()) {
attachmentContainer.isVisible = false
attachmentContainer.removeAllViews()
} else {
attachmentContainer.isVisible = true
AttachmentInflater(
context = context,
container = attachmentContainer,
message = message,
groups = groups,
profiles = profiles
).inflate()
}
}
if (bubble != null) {
val padding =
AndroidUtils.px(if (!message.attachments.isNullOrEmpty()) 4 else 15).roundToInt()
bubble.setPadding(padding)
// TODO: 9/23/2021 use external function
bubble.background =
if (!message.attachments.isNullOrEmpty() && message.attachments!![0] is VkSticker) null
else {
if (message.isOut) {
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormalOut
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleOut
else backgroundNormalOut
} else {
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormalIn
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleIn
else backgroundNormalIn
}
}
}
// TODO: 9/23/2021 use external function
bubbleStroke?.background =
if (bubble?.background == null) null else {
if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundStrokeOut
else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleStrokeOut
else backgroundStrokeOut
}
if (bubble != null && text != null) {
if (message.text == null) {
text.isVisible = false
bubble.isVisible = !message.attachments.isNullOrEmpty()
bubbleStroke?.isVisible = bubble.isVisible
} else {
text.isVisible = true
bubble.isVisible = true
bubbleStroke?.isVisible = true
text.text = VkUtils.prepareMessageText(message.text)
}
}
if (avatar != null) {
val avatarUrl = when {
message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200
message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200
else -> null
}
avatar.load(avatarUrl) { crossfade(100) }
}
spacer?.isVisible = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message)
if (message.isPeerChat()) {
val fromDiffSender = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message)
val fiveMinAgo = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message)
val change = (prevMessage?.date ?: 0) - message.date
Log.d(
"Fast::MessagesPreparator",
"text: ${message.text}; prevText: ${prevMessage?.text}; time change: $change; fromDiffSender: $fromDiffSender; fiveMinAgo: $fiveMinAgo; "
)
title?.isVisible = fromDiffSender || fiveMinAgo
avatar?.isInvisible = fromDiffSender && fiveMinAgo
} else {
title?.isVisible = false
avatar?.isVisible = false
}
if (title != null) {
val titleString = when {
message.isUser() && messageUser != null -> messageUser.firstName
message.isGroup() && messageGroup != null -> messageGroup.name
else -> null
}
title.text = titleString
title.measure(0, 0)
if (bubble != null) {
if (title.isVisible) {
bubble.minimumWidth = title.measuredWidth + 60
} else {
bubble.minimumWidth = 0
}
}
}
attachmentSpacer?.isVisible =
!message.attachments.isNullOrEmpty() && text?.isVisible == true
time?.text = SimpleDateFormat("HH:mm", Locale.getDefault()).format(message.date * 1000L)
}
}
@@ -73,4 +73,11 @@ object AndroidUtils {
return color
}
fun bytesToHumanReadableSize(bytes: Double) = when {
bytes >= 1 shl 30 -> "%.1f GB".format(bytes / (1 shl 30))
bytes >= 1 shl 20 -> "%.1f MB".format(bytes / (1 shl 20))
bytes >= 1 shl 10 -> "%.0f KB".format(bytes / (1 shl 10))
else -> "$bytes B"
}
}
@@ -0,0 +1,79 @@
package com.meloda.fast.widget;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.TypedValue;
import android.widget.FrameLayout;
public class RoundedCornerLayout extends FrameLayout {
private final static float CORNER_RADIUS = 40.0f;
private Bitmap maskBitmap;
private Paint paint, maskPaint;
private float cornerRadius;
public RoundedCornerLayout(Context context) {
super(context);
init(context, null, 0);
}
public RoundedCornerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs, 0);
}
public RoundedCornerLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs, defStyle);
}
private void init(Context context, AttributeSet attrs, int defStyle) {
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
cornerRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, CORNER_RADIUS, metrics);
paint = new Paint(Paint.ANTI_ALIAS_FLAG);
maskPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
maskPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
setWillNotDraw(false);
}
@Override
public void draw(Canvas canvas) {
Bitmap offscreenBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas offscreenCanvas = new Canvas(offscreenBitmap);
super.draw(offscreenCanvas);
if (maskBitmap == null) {
maskBitmap = createMask(getWidth(), getHeight());
}
offscreenCanvas.drawBitmap(maskBitmap, 0f, 0f, maskPaint);
canvas.drawBitmap(offscreenBitmap, 0f, 0f, paint);
}
private Bitmap createMask(int width, int height) {
Bitmap mask = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8);
Canvas canvas = new Canvas(mask);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColor(Color.WHITE);
canvas.drawRect(0, 0, width, height, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
canvas.drawRoundRect(new RectF(0, 0, width, height), cornerRadius, cornerRadius, paint);
return mask;
}
}
@@ -0,0 +1,95 @@
package com.meloda.fast.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Path
import android.graphics.RectF
import android.graphics.Region
import android.util.AttributeSet
import android.widget.FrameLayout
import com.meloda.fast.R
class RoundedFrameLayout : FrameLayout {
/**
* The corners than can be changed
*/
private var topLeftCornerRadius = 0f
private var topRightCornerRadius = 0f
private var bottomLeftCornerRadius = 0f
private var bottomRightCornerRadius = 0f
constructor(context: Context) : super(context) {
init(context, null, 0)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs, 0)
}
constructor(
context: Context,
attrs: AttributeSet?,
defStyleAttr: Int
) : super(context, attrs, defStyleAttr) {
init(context, attrs, defStyleAttr)
}
private fun init(context: Context, attrs: AttributeSet?, defStyle: Int) {
val typedArray = context.obtainStyledAttributes(
attrs,
R.styleable.RoundedFrameLayout, 0, 0
)
topLeftCornerRadius =
typedArray.getDimension(R.styleable.RoundedFrameLayout_topLeftCornerRadius, 0f)
topRightCornerRadius =
typedArray.getDimension(R.styleable.RoundedFrameLayout_topRightCornerRadius, 0f)
bottomLeftCornerRadius =
typedArray.getDimension(R.styleable.RoundedFrameLayout_bottomLeftCornerRadius, 0f)
bottomRightCornerRadius =
typedArray.getDimension(R.styleable.RoundedFrameLayout_bottomRightCornerRadius, 0f)
typedArray.recycle()
setLayerType(LAYER_TYPE_HARDWARE, null)
}
override fun dispatchDraw(canvas: Canvas) {
val count: Int = canvas.save()
val path = Path()
val cornerDimensions = floatArrayOf(
topLeftCornerRadius, topLeftCornerRadius,
topRightCornerRadius, topRightCornerRadius,
bottomRightCornerRadius, bottomRightCornerRadius,
bottomLeftCornerRadius, bottomLeftCornerRadius
)
path.addRoundRect(
RectF(0f, 0f, canvas.width.toFloat(), canvas.height.toFloat()),
cornerDimensions,
Path.Direction.CW
)
canvas.clipPath(path, Region.Op.INTERSECT)
canvas.clipPath(path)
super.dispatchDraw(canvas)
canvas.restoreToCount(count)
}
fun setTopLeftCornerRadius(topLeftCornerRadius: Float) {
this.topLeftCornerRadius = topLeftCornerRadius
invalidate()
}
fun setTopRightCornerRadius(topRightCornerRadius: Float) {
this.topRightCornerRadius = topRightCornerRadius
invalidate()
}
fun setBottomLeftCornerRadius(bottomLeftCornerRadius: Float) {
this.bottomLeftCornerRadius = bottomLeftCornerRadius
invalidate()
}
fun setBottomRightCornerRadius(bottomRightCornerRadius: Float) {
this.bottomRightCornerRadius = bottomRightCornerRadius
invalidate()
}
}
@@ -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="M15.1,19.37l1,1.74c-0.96,0.44 -2.01,0.73 -3.1,0.84v-2.02C13.74,19.84 14.44,19.65 15.1,19.37zM4.07,13H2.05c0.11,1.1 0.4,2.14 0.84,3.1l1.74,-1C4.35,14.44 4.16,13.74 4.07,13zM15.1,4.63l1,-1.74C15.14,2.45 14.1,2.16 13,2.05v2.02C13.74,4.16 14.44,4.35 15.1,4.63zM19.93,11h2.02c-0.11,-1.1 -0.4,-2.14 -0.84,-3.1l-1.74,1C19.65,9.56 19.84,10.26 19.93,11zM8.9,19.37l-1,1.74c0.96,0.44 2.01,0.73 3.1,0.84v-2.02C10.26,19.84 9.56,19.65 8.9,19.37zM11,4.07V2.05c-1.1,0.11 -2.14,0.4 -3.1,0.84l1,1.74C9.56,4.35 10.26,4.16 11,4.07zM18.36,7.17l1.74,-1.01c-0.63,-0.87 -1.4,-1.64 -2.27,-2.27l-1.01,1.74C17.41,6.08 17.92,6.59 18.36,7.17zM4.63,8.9l-1.74,-1C2.45,8.86 2.16,9.9 2.05,11h2.02C4.16,10.26 4.35,9.56 4.63,8.9zM19.93,13c-0.09,0.74 -0.28,1.44 -0.56,2.1l1.74,1c0.44,-0.96 0.73,-2.01 0.84,-3.1H19.93zM16.83,18.36l1.01,1.74c0.87,-0.63 1.64,-1.4 2.27,-2.27l-1.74,-1.01C17.92,17.41 17.41,17.92 16.83,18.36zM7.17,5.64L6.17,3.89C5.29,4.53 4.53,5.29 3.9,6.17l1.74,1.01C6.08,6.59 6.59,6.08 7.17,5.64zM5.64,16.83L3.9,17.83c0.63,0.87 1.4,1.64 2.27,2.27l1.01,-1.74C6.59,17.92 6.08,17.41 5.64,16.83zM13,7h-2v5.41l4.29,4.29l1.41,-1.41L13,11.59V7z" />
</vector>
@@ -144,7 +144,7 @@
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:maxLines="1"
android:textColor="@color/n1_900"
android:textSize="24sp"
tools:text="@tools:sample/full_names" />
@@ -154,6 +154,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.7"
android:maxLines="1"
android:textColor="@color/n1_900"
tools:text="Online" />
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="4dp">
<FrameLayout
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="top"
android:layout_marginTop="2dp"
android:background="@drawable/ic_play_button_circle_background"
android:backgroundTint="@color/a3_200">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_round_play_arrow_24"
app:tint="@color/a3_700" />
</FrameLayout>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="@font/google_sans_regular"
android:maxLines="1"
android:textColor="@color/n1_800"
android:textSize="18sp"
tools:text="Даня, дай Фаст" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/artist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.8"
android:fontFamily="@font/roboto_regular"
android:textColor="@color/n1_800"
tools:text="Эльчин Оруджев | 0:36" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="4dp">
<FrameLayout
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="top"
android:layout_marginTop="2dp"
android:background="@drawable/ic_play_button_circle_background"
android:backgroundTint="@color/a3_200">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_attachment_file"
app:tint="@color/a3_700" />
</FrameLayout>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:fontFamily="@font/google_sans_regular"
android:maxLines="1"
android:textColor="@color/n1_800"
android:textSize="18sp"
tools:text="Kids" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.8"
android:fontFamily="@font/roboto_regular"
android:textColor="@color/n1_800"
tools:text="3.28 TB" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:padding="4dp">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/preview"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
android:scaleType="centerCrop"
tools:src="@tools:sample/avatars" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?textColorPrimary"
android:textSize="18sp"
app:fontFamily="@font/google_sans_regular"
tools:text="melod1n" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/caption"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.8"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?textColorPrimary"
app:fontFamily="@font/roboto_regular"
tools:text="vk.com/melod1n" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
@@ -1,99 +0,0 @@
<?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/photosContainer"
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>
@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<com.meloda.fast.widget.BoundedFrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/photosContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:orientation="vertical" />
</com.meloda.fast.widget.BoundedFrameLayout>
</layout>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
@@ -1,41 +0,0 @@
<?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">
<Space
android:id="@+id/spacer"
android:layout_width="match_parent"
android:layout_height="10dp"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:src="@tools:sample/backgrounds/scenic" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
@@ -1,34 +0,0 @@
<?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:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="2.5dp">
<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" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:src="@tools:sample/backgrounds/scenic" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
@@ -1,99 +0,0 @@
<?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>
@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="4dp">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/postTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
android:alpha="0.9"
android:text="@string/message_attachments_wall"
android:textColor="?textColorPrimary"
android:textSize="12sp" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/avatar"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginEnd="8dp"
tools:src="@tools:sample/avatars" />
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?textColorPrimary"
android:textSize="18sp"
app:fontFamily="@font/google_sans_regular"
tools:text="Typical Programmer" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="0.8"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?textColorPrimary"
tools:text="1 hour ago" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
+33 -4
View File
@@ -26,7 +26,8 @@
android:id="@+id/spacer"
android:layout_width="match_parent"
android:layout_height="10dp"
android:visibility="gone" />
android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
@@ -44,12 +45,14 @@
android:gravity="bottom"
android:orientation="horizontal">
<com.meloda.fast.widget.BoundedFrameLayout
<com.meloda.fast.widget.BoundedLinearLayout
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"
android:orientation="vertical"
android:padding="15dp"
tools:ignore="UselessParent">
<com.google.android.material.textview.MaterialTextView
@@ -57,11 +60,24 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:padding="15dp"
android:autoLink="all"
android:textColor="@color/n1_800"
tools:text="This" />
</com.meloda.fast.widget.BoundedFrameLayout>
<Space
android:id="@+id/attachmentSpacer"
android:layout_width="wrap_content"
android:layout_height="5dp"
android:visibility="gone" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/attachmentContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone" />
</com.meloda.fast.widget.BoundedLinearLayout>
<com.meloda.fast.widget.CircleImageView
android:id="@+id/unread"
@@ -72,6 +88,19 @@
android:src="@color/a3_200" />
</androidx.appcompat.widget.LinearLayoutCompat>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginStart="12dp"
android:textColor="?textColorSecondaryVariant"
android:visibility="gone"
tools:layout_height="18dp"
tools:text="12:00"
tools:visibility="visible" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
+21 -9
View File
@@ -38,25 +38,38 @@
android:padding="1.5dp"
tools:ignore="UselessParent">
<com.meloda.fast.widget.BoundedFrameLayout
<com.meloda.fast.widget.BoundedLinearLayout
android:id="@+id/bubble"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/ic_message_out_background">
android:background="@drawable/ic_message_out_background"
android:orientation="vertical"
android:padding="15dp">
<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|start"
android:padding="15dp"
android:textColor="@color/n1_900"
tools:text="This is test" />
</com.meloda.fast.widget.BoundedFrameLayout>
</FrameLayout>
<Space
android:id="@+id/attachmentSpacer"
android:layout_width="wrap_content"
android:layout_height="5dp"
android:visibility="gone" />
<androidx.appcompat.widget.LinearLayoutCompat
android:id="@+id/attachmentContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone" />
</com.meloda.fast.widget.BoundedLinearLayout>
</FrameLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/time"
@@ -65,12 +78,11 @@
android:layout_gravity="end"
android:layout_marginEnd="12dp"
android:textColor="?textColorSecondaryVariant"
android:visibility="gone"
tools:layout_height="18dp"
tools:text="12:00" />
tools:text="12:00"
tools:visibility="visible" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
+7
View File
@@ -25,6 +25,13 @@
<attr name="bounded_height" format="dimension" />
</declare-styleable>
<declare-styleable name="RoundedFrameLayout">
<attr name="topLeftCornerRadius" format="dimension" />
<attr name="topRightCornerRadius" format="dimension" />
<attr name="bottomLeftCornerRadius" format="dimension" />
<attr name="bottomRightCornerRadius" format="dimension" />
</declare-styleable>
<declare-styleable name="WrapTextView">
<attr name="fixWrap" format="boolean" />
</declare-styleable>
+32
View File
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<plurals name="attachment_photos">
<item quantity="one">@string/message_attachments_photo_one</item>
<item quantity="few">@string/message_attachments_photo_few</item>
<item quantity="many">@string/message_attachments_photo_many</item>
<item quantity="other">@string/message_attachments_photo_other</item>
</plurals>
<plurals name="attachment_videos">
<item quantity="one">@string/message_attachments_video_one</item>
<item quantity="few">@string/message_attachments_video_few</item>
<item quantity="many">@string/message_attachments_video_many</item>
<item quantity="other">@string/message_attachments_video_other</item>
</plurals>
<plurals name="attachment_audios">
<item quantity="one">@string/message_attachments_audio_one</item>
<item quantity="few">@string/message_attachments_audio_few</item>
<item quantity="many">@string/message_attachments_audio_many</item>
<item quantity="other">@string/message_attachments_audio_other</item>
</plurals>
<plurals name="attachment_files">
<item quantity="one">@string/message_attachments_files_one</item>
<item quantity="few">@string/message_attachments_files_few</item>
<item quantity="many">@string/message_attachments_files_many</item>
<item quantity="other">@string/message_attachments_files_other</item>
</plurals>
</resources>
+59 -1
View File
@@ -43,7 +43,7 @@
<string name="week_short">W</string>
<string name="day_short">D</string>
<string name="time_now">Now</string>
<string name="message_input_hint">Start typing here...</string>
<string name="message_input_hint">Start typing here&#8230;</string>
<string name="input_login_hint">Input login</string>
<string name="input_password_hint">Input password</string>
<string name="input_code_hint">Input code</string>
@@ -51,4 +51,62 @@
<string name="unknown_error_occurred">Unknown error occurred</string>
<string name="authorization_failed">Authorization failed</string>
<string name="message_action_chat_created">%s created «%s»</string>
<string name="message_action_chat_renamed">%s renamed chat to «%s»</string>
<string name="message_action_chat_photo_update">%s updated the chat photo</string>
<string name="message_action_chat_photo_remove">%s deleted the chat photo</string>
<string name="message_action_chat_user_left">%s left the chat</string>
<string name="message_action_chat_user_kicked">%s kicked %s</string>
<string name="message_action_chat_user_returned">%s returned to chat</string>
<string name="message_action_chat_user_invited">%s invited %s</string>
<string name="message_action_chat_user_joined_by_link">%s joined the chat via link</string>
<string name="message_action_chat_user_joined_by_call_link">%s joined the call via link</string>
<string name="message_action_chat_pin_message">%s pinned message</string>
<string name="message_action_chat_unpin_message">%s unpinned message</string>
<string name="message_action_chat_screenshot">%s took a screenshot</string>
<string name="message_action_chat_style_update">%s changed chat theme</string>
<string name="message_attachments_photo_one">Photo</string>
<string name="message_attachments_photo_few">%d photos</string>
<string name="message_attachments_photo_many">%d photos</string>
<string name="message_attachments_photo_other">%d photos</string>
<string name="message_attachments_video_one">Video</string>
<string name="message_attachments_video_few">%d videos</string>
<string name="message_attachments_video_many">%d videos</string>
<string name="message_attachments_video_other">%d videos</string>
<string name="message_attachments_audio_one">Audio</string>
<string name="message_attachments_audio_few">%d audios</string>
<string name="message_attachments_audio_many">%d audios</string>
<string name="message_attachments_audio_other">%d audios</string>
<string name="message_attachments_files_one">File</string>
<string name="message_attachments_files_few">%d files</string>
<string name="message_attachments_files_many">%d files</string>
<string name="message_attachments_files_other">%d files</string>
<string name="message_attachments_voice">Voice message</string>
<string name="message_attachments_link">Link</string>
<string name="message_attachments_mini_app">Mini App</string>
<string name="message_attachments_sticker">Sticker</string>
<string name="message_attachments_gift">Gift</string>
<string name="message_attachments_wall">Post</string>
<string name="message_attachments_graffiti">Graffiti</string>
<string name="message_attachments_poll">Poll</string>
<string name="message_attachments_wall_reply">Wall comment</string>
<string name="message_attachments_call">Call</string>
<string name="message_attachments_call_in_progress">Current call</string>
<string name="message_attachments_event">Event</string>
<string name="message_attachments_curator">Curator</string>
<string name="file_size_in_bytes">%d bytes</string>
<string name="post_type_community">Community post</string>
<string name="post_type_user">User post</string>
<string name="post_type_unknown">Post</string>
<string name="message_attachments_story">Story</string>
</resources>