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
+5 -3
View File
@@ -19,7 +19,7 @@ android {
defaultConfig { defaultConfig {
applicationId = "com.meloda.fast" applicationId = "com.meloda.fast"
minSdk = 23 minSdk = 23
targetSdk = 31 targetSdk = 30
versionCode = 1 versionCode = 1
versionName = "1.0" versionName = "1.0"
@@ -75,15 +75,17 @@ kapt {
} }
dependencies { dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.30") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.31")
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5")
implementation("androidx.work:work-runtime-ktx:2.6.0") implementation("androidx.work:work-runtime-ktx:2.6.0")
implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.appcompat:appcompat:1.4.0-alpha03") implementation("androidx.appcompat:appcompat:1.4.0-alpha03")
implementation("com.google.android.material:material:1.5.0-alpha03") implementation("com.google.android.material:material:1.5.0-alpha03")
implementation("androidx.core:core-ktx:1.7.0-alpha02") implementation("androidx.core:core-ktx:1.7.0-beta01")
implementation("androidx.preference:preference-ktx:1.1.1") implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
implementation("androidx.recyclerview:recyclerview:1.2.1") implementation("androidx.recyclerview:recyclerview:1.2.1")
-9
View File
@@ -28,15 +28,6 @@
</intent-filter> </intent-filter>
</activity> </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 <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"
@@ -2,6 +2,7 @@ package com.meloda.fast.activity
import android.os.Bundle import android.os.Bundle
import android.viewbinding.library.activity.viewBinding import android.viewbinding.library.activity.viewBinding
import androidx.lifecycle.lifecycleScope
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.base.BaseActivity import com.meloda.fast.base.BaseActivity
import com.meloda.fast.databinding.ActivityMainBinding import com.meloda.fast.databinding.ActivityMainBinding
@@ -14,6 +15,10 @@ class MainActivity : BaseActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) 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.attachments.*
import com.meloda.fast.api.model.base.BaseVkMessage import com.meloda.fast.api.model.base.BaseVkMessage
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
import com.meloda.fast.api.network.VkErrors
object VkUtils { object VkUtils {
fun isValidationRequired(throwable: Throwable): Boolean { fun prepareMessageText(text: String, forConversations: Boolean? = null): String {
if (throwable !is VKException) return false return text.apply {
return throwable.error == VkErrors.NEED_VALIDATION if (forConversations == true) replace("\n", "")
replace("&amp", "&")
}
} }
fun isCaptchaRequired(throwable: Throwable): Boolean { fun isPreviousMessageSentFiveMinutesAgo(prevMessage: VkMessage?, message: VkMessage?) =
if (throwable !is VKException) return false prevMessage != null && message != null && (message.date - prevMessage.date >= 300)
return throwable.error == VkErrors.NEED_CAPTCHA
}
fun prepareMessageText(text: String?): String? { fun isPreviousMessageFromDifferentSender(prevMessage: VkMessage?, message: VkMessage?) =
if (text == null) return null prevMessage != null && message != null && prevMessage.fromId != message.fromId
return text
.replace("\n", " ")
.replace("&amp", "&")
}
fun parseForwards(baseForwards: List<BaseVkMessage>?): List<VkMessage>? { fun parseForwards(baseForwards: List<BaseVkMessage>?): List<VkMessage>? {
if (baseForwards.isNullOrEmpty()) return null if (baseForwards.isNullOrEmpty()) return null
@@ -64,21 +59,15 @@ object VkUtils {
} }
BaseVkAttachmentItem.AttachmentType.AUDIO -> { BaseVkAttachmentItem.AttachmentType.AUDIO -> {
val audio = baseAttachment.audio ?: continue val audio = baseAttachment.audio ?: continue
attachments += VkAudio( attachments += audio.asVkAudio()
link = audio.url
)
} }
BaseVkAttachmentItem.AttachmentType.FILE -> { BaseVkAttachmentItem.AttachmentType.FILE -> {
val file = baseAttachment.file ?: continue val file = baseAttachment.file ?: continue
attachments += VkFile( attachments += file.asVkFile()
link = file.url
)
} }
BaseVkAttachmentItem.AttachmentType.LINK -> { BaseVkAttachmentItem.AttachmentType.LINK -> {
val link = baseAttachment.link ?: continue val link = baseAttachment.link ?: continue
attachments += VkLink( attachments += link.asVkLink()
link = link.url
)
} }
BaseVkAttachmentItem.AttachmentType.MINI_APP -> { BaseVkAttachmentItem.AttachmentType.MINI_APP -> {
val miniApp = baseAttachment.miniApp ?: continue val miniApp = baseAttachment.miniApp ?: continue
@@ -89,7 +78,7 @@ object VkUtils {
BaseVkAttachmentItem.AttachmentType.VOICE -> { BaseVkAttachmentItem.AttachmentType.VOICE -> {
val voiceMessage = baseAttachment.voiceMessage ?: continue val voiceMessage = baseAttachment.voiceMessage ?: continue
attachments += VkVoiceMessage( attachments += VkVoiceMessage(
link = voiceMessage.linkMp3 link = voiceMessage.link_mp3
) )
} }
BaseVkAttachmentItem.AttachmentType.STICKER -> { BaseVkAttachmentItem.AttachmentType.STICKER -> {
@@ -99,14 +88,12 @@ object VkUtils {
BaseVkAttachmentItem.AttachmentType.GIFT -> { BaseVkAttachmentItem.AttachmentType.GIFT -> {
val gift = baseAttachment.gift ?: continue val gift = baseAttachment.gift ?: continue
attachments += VkGift( attachments += VkGift(
link = gift.thumb48 link = gift.thumb_48
) )
} }
BaseVkAttachmentItem.AttachmentType.WALL -> { BaseVkAttachmentItem.AttachmentType.WALL -> {
val wall = baseAttachment.wall ?: continue val wall = baseAttachment.wall ?: continue
attachments += VkWall( attachments += wall.asVkWall()
id = wall.id
)
} }
BaseVkAttachmentItem.AttachmentType.GRAFFITI -> { BaseVkAttachmentItem.AttachmentType.GRAFFITI -> {
val graffiti = baseAttachment.graffiti ?: continue val graffiti = baseAttachment.graffiti ?: continue
@@ -129,15 +116,27 @@ object VkUtils {
BaseVkAttachmentItem.AttachmentType.CALL -> { BaseVkAttachmentItem.AttachmentType.CALL -> {
val call = baseAttachment.call ?: continue val call = baseAttachment.call ?: continue
attachments += VkCall( attachments += VkCall(
initiatorId = call.initiatorId initiatorId = call.initiator_id
) )
} }
BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> { BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> {
val groupCall = baseAttachment.groupCall ?: continue val groupCall = baseAttachment.groupCall ?: continue
attachments += VkGroupCall( 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 else -> continue
} }
} }
@@ -145,177 +144,12 @@ object VkUtils {
return attachments 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( fun getActionMessageText(
context: Context,
message: VkMessage, message: VkMessage,
youPrefix: String, youPrefix: String,
profiles: HashMap<Int, VkUser>? = null, profiles: Map<Int, VkUser>? = null,
groups: HashMap<Int, VkGroup>? = null, groups: Map<Int, VkGroup>? = null,
messageUser: VkUser? = null, messageUser: VkUser? = null,
messageGroup: VkGroup? = null messageGroup: VkGroup? = null
): SpannableString? { ): SpannableString? {
@@ -330,7 +164,8 @@ object VkUtils {
else -> return null else -> return null
} ?: return null } ?: return null
val spanText = "$prefix created «$text»" val spanText =
context.getString(R.string.message_action_chat_created, prefix, text)
SpannableString(spanText).also { SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
@@ -351,8 +186,8 @@ object VkUtils {
else -> return null else -> return null
} ?: 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) val startIndex = spanText.indexOf(text)
SpannableString(spanText).also { SpannableString(spanText).also {
@@ -370,7 +205,9 @@ object VkUtils {
else -> return null else -> return null
} ?: 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 { SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
} }
@@ -383,7 +220,9 @@ object VkUtils {
else -> return null else -> return null
} ?: 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 { SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
} }
@@ -402,7 +241,10 @@ object VkUtils {
if (memberId == message.fromId) { if (memberId == message.fromId) {
val prefix = if (memberId == UserConfig.userId) youPrefix val prefix = if (memberId == UserConfig.userId) youPrefix
else actionUser.toString() else actionUser.toString()
val spanText = "$prefix left the chat"
val spanText =
context.getString(R.string.message_action_chat_user_left, prefix)
SpannableString(spanText).also { SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
} }
@@ -410,11 +252,17 @@ object VkUtils {
val prefix = val prefix =
if (message.fromId == UserConfig.userId) youPrefix if (message.fromId == UserConfig.userId) youPrefix
else messageUser?.toString() ?: messageGroup?.toString() ?: "..." else messageUser?.toString() ?: messageGroup?.toString() ?: "..."
val postfix = val postfix =
if (memberId == UserConfig.userId) youPrefix.lowercase() if (memberId == UserConfig.userId) youPrefix.lowercase()
else actionUser.toString() 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) val startIndex = spanText.indexOf(postfix)
SpannableString(spanText).also { SpannableString(spanText).also {
@@ -439,18 +287,27 @@ object VkUtils {
if (memberId == message.fromId) { if (memberId == message.fromId) {
val prefix = if (memberId == UserConfig.userId) youPrefix val prefix = if (memberId == UserConfig.userId) youPrefix
else actionUser.toString() else actionUser.toString()
val spanText = "$prefix returned the chat"
val spanText =
context.getString(R.string.message_action_chat_user_returned, prefix)
SpannableString(spanText).also { SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
} }
} else { } else {
val prefix = if (message.fromId == UserConfig.userId) youPrefix val prefix = if (message.fromId == UserConfig.userId) youPrefix
else messageUser?.toString() ?: messageGroup?.toString() ?: "..." else messageUser?.toString() ?: messageGroup?.toString() ?: "..."
val postfix = val postfix =
if (memberId == UserConfig.userId) youPrefix.lowercase() if (memberId == UserConfig.userId) youPrefix.lowercase()
else actionUser.toString() 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) val startIndex = spanText.indexOf(postfix)
SpannableString(spanText).also { SpannableString(spanText).also {
@@ -468,7 +325,9 @@ object VkUtils {
else -> return null else -> return null
} ?: 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 { SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
} }
@@ -480,7 +339,9 @@ object VkUtils {
else -> return null else -> return null
} ?: 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 { SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
} }
@@ -493,16 +354,11 @@ object VkUtils {
else -> return null else -> return null
} ?: return null } ?: return null
val actionMessage = message.actionMessage ?: return null val spanText =
context.getString(R.string.message_action_chat_pin_message, prefix).trim()
val spanText = "$prefix pinned message «$actionMessage»"
val startIndex = spanText.indexOf(actionMessage)
SpannableString(spanText).also { SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
it.setSpan(
StyleSpan(Typeface.BOLD), startIndex, startIndex + actionMessage.length, 0
)
} }
} }
VkMessage.Action.CHAT_UNPIN_MESSAGE -> { VkMessage.Action.CHAT_UNPIN_MESSAGE -> {
@@ -513,7 +369,9 @@ object VkUtils {
else -> return null else -> return null
} ?: return null } ?: return null
val spanText = "$prefix unpinned message" val spanText =
context.getString(R.string.message_action_chat_unpin_message, prefix)
SpannableString(spanText).also { SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
} }
@@ -526,7 +384,9 @@ object VkUtils {
else -> return null else -> return null
} ?: return null } ?: return null
val spanText = "$prefix took a screenshot" val spanText =
context.getString(R.string.message_action_chat_screenshot, prefix)
SpannableString(spanText).also { SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0)
} }
@@ -538,7 +398,9 @@ object VkUtils {
else -> return null else -> return null
} ?: 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 { SpannableString(spanText).also {
it.setSpan(StyleSpan(Typeface.BOLD), 0, prefix.length, 0) 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? { fun getForwardsConversationText(context: Context, message: VkMessage): String? {
if (message.forwards.isNullOrEmpty()) return null if (message.forwards.isNullOrEmpty()) return null
@@ -570,10 +452,19 @@ object VkUtils {
return message.attachments?.let { attachments -> return message.attachments?.let { attachments ->
if (attachments.size == 1) { if (attachments.size == 1) {
getAttachmentTypeByClass(attachments[0])?.let { getAttachmentTextByType(it) } getAttachmentTypeByClass(attachments[0])?.let {
getAttachmentTextByType(
context,
it
)
}
} else { } else {
if (isAttachmentsHaveOneType(attachments)) { if (isAttachmentsHaveOneType(attachments)) {
getAttachmentTypeByClass(attachments[0])?.let { getAttachmentTextByType(it) } getAttachmentTypeByClass(attachments[0])?.let {
getAttachmentTextByType(
context, it, attachments.size
)
}
} else { } else {
context.getString(R.string.message_attachments_many) 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.WALL_REPLY -> R.drawable.ic_attachment_wall_reply
BaseVkAttachmentItem.AttachmentType.CALL -> R.drawable.ic_attachment_call BaseVkAttachmentItem.AttachmentType.CALL -> R.drawable.ic_attachment_call
BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> R.drawable.ic_attachment_group_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) return ContextCompat.getDrawable(context, resId)
@@ -657,14 +550,56 @@ object VkUtils {
is VkWallReply -> BaseVkAttachmentItem.AttachmentType.WALL_REPLY is VkWallReply -> BaseVkAttachmentItem.AttachmentType.WALL_REPLY
is VkCall -> BaseVkAttachmentItem.AttachmentType.CALL is VkCall -> BaseVkAttachmentItem.AttachmentType.CALL
is VkGroupCall -> BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS 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 else -> null
} }
} }
fun getAttachmentTextByType(attachmentType: BaseVkAttachmentItem.AttachmentType): String? { fun getAttachmentTextByType(
context: Context,
attachmentType: BaseVkAttachmentItem.AttachmentType,
size: Int = 1
): String {
return when (attachmentType) { 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 else -> attachmentType.value
} }
} }
} }
@@ -5,7 +5,11 @@ import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkAudio( data class VkAudio(
val link: String val id: Int,
val title: String,
val artist: String,
val url: String,
val duration: Int
) : VkAttachment() { ) : VkAttachment() {
@IgnoredOnParcel @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 @Parcelize
data class VkFile( data class VkFile(
val link: String val id: Int,
val title: String,
val ext: String,
val size: Int,
val url: String
) : VkAttachment() { ) : VkAttachment() {
@IgnoredOnParcel @IgnoredOnParcel
@@ -5,7 +5,12 @@ import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkLink( 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() { ) : VkAttachment() {
@IgnoredOnParcel @IgnoredOnParcel
@@ -1,6 +1,6 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.Size import com.meloda.fast.api.model.base.attachments.BaseVkPhoto
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -12,7 +12,7 @@ data class VkPhoto(
val ownerId: Int, val ownerId: Int,
val hasTags: Boolean, val hasTags: Boolean,
val accessKey: String?, val accessKey: String?,
val sizes: List<Size>, val sizes: List<BaseVkPhoto.Size>,
val text: String, val text: String,
val userId: Int? val userId: Int?
) : VkAttachment() { ) : VkAttachment() {
@@ -20,7 +20,7 @@ data class VkPhoto(
@IgnoredOnParcel @IgnoredOnParcel
val className: String = this::class.java.name val className: String = this::class.java.name
fun sizeOfType(type: Char): Size? { fun sizeOfType(type: Char): BaseVkPhoto.Size? {
for (size in sizes) { for (size in sizes) {
if (size.type == type.toString()) if (size.type == type.toString())
return size return size
@@ -1,7 +1,6 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.BaseVkSticker import com.meloda.fast.api.model.base.attachments.BaseVkSticker
import com.meloda.fast.api.model.base.attachments.StickerSize
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -16,7 +15,7 @@ data class VkSticker(
@IgnoredOnParcel @IgnoredOnParcel
val className: String = this::class.java.name val className: String = this::class.java.name
fun urlForSize(@StickerSize size: Int): String? { fun urlForSize(size: Int): String? {
for (image in images) { for (image in images) {
if (image.width == size) return image.url if (image.width == size) return image.url
} }
@@ -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( data class VkVideo(
val id: Int, val id: Int,
val images: List<BaseVkVideo.Image>, val images: List<BaseVkVideo.Image>,
val firstFrames: List<BaseVkVideo.FirstFrame> val firstFrames: List<BaseVkVideo.FirstFrame>?
) : VkAttachment() { ) : VkAttachment() {
@IgnoredOnParcel @IgnoredOnParcel
@@ -1,11 +1,23 @@
package com.meloda.fast.api.model.attachments package com.meloda.fast.api.model.attachments
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class VkWall( data class VkWall(
val id: Int val id: Int,
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() { ) : VkAttachment() {
@IgnoredOnParcel @IgnoredOnParcel
@@ -1,90 +1,70 @@
package com.meloda.fast.api.model.base package com.meloda.fast.api.model.base
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.base.attachments.BaseVkGroupCall
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkConversation( data class BaseVkConversation(
val peer: Peer, val peer: Peer,
@SerializedName("last_message_id") val last_message_id: Int,
val lastMessageId: Int, val in_read: Int,
@SerializedName("in_read") val out_read: Int,
val inRead: Int, val sort_id: SortId,
@SerializedName("out_read") val last_conversation_message_id: Int,
val outRead: Int, val is_marked_unread: Boolean,
@SerializedName("sort_id")
val sortId: SortId,
@SerializedName("last_conversation_message_id")
val lastConversationMessageId: Int,
@SerializedName("is_marked_unread")
val isMarkedUnread: Boolean,
val important: Boolean, val important: Boolean,
@SerializedName("push_settings") val push_settings: PushSettings,
val pushSettings: PushSettings, val can_write: CanWrite,
@SerializedName("can_write") val can_send_money: Boolean,
val canWrite: CanWrite, val can_receive_money: Boolean,
@SerializedName("can_send_money") val chat_settings: ChatSettings?,
val canSendMoney: Boolean, val call_in_progress: CallInProgress?,
@SerializedName("can_receive_money") val unread_count: Int?
val canReceiveMoney: Boolean,
@SerializedName("chat_settings")
val chatSettings: ChatSettings?,
@SerializedName("call_in_progress")
val callInProgress: CallInProgress?,
@SerializedName("unread_count")
val unreadCount: Int?
) : Parcelable { ) : Parcelable {
fun asVkConversation(lastMessage: VkMessage? = null) = VkConversation( fun asVkConversation(lastMessage: VkMessage? = null) = VkConversation(
id = peer.id, id = peer.id,
title = chatSettings?.title, title = chat_settings?.title,
photo200 = chatSettings?.photo?.photo200, photo200 = chat_settings?.photo?.photo_200,
type = peer.type, type = peer.type,
callInProgress = callInProgress != null, callInProgress = call_in_progress != null,
isPhantom = chatSettings?.isDisappearing == true, isPhantom = chat_settings?.is_disappearing == true,
lastConversationMessageId = lastConversationMessageId, lastConversationMessageId = last_conversation_message_id,
inRead = inRead, inRead = in_read,
outRead = outRead, outRead = out_read,
isMarkedUnread = isMarkedUnread, isMarkedUnread = is_marked_unread,
lastMessageId = lastMessageId, lastMessageId = last_message_id,
unreadCount = unreadCount, unreadCount = unread_count,
membersCount = chatSettings?.membersCount, membersCount = chat_settings?.members_count,
ownerId = chatSettings?.ownerId, ownerId = chat_settings?.owner_id,
isPinned = sortId.majorId > 0 isPinned = sort_id.major_id > 0
).apply { ).apply {
this.lastMessage = lastMessage this.lastMessage = lastMessage
this.pinnedMessage = chatSettings?.pinnedMessage?.asVkMessage() this.pinnedMessage = chat_settings?.pinned_message?.asVkMessage()
} }
@Parcelize @Parcelize
data class Peer( data class Peer(
val id: Int, val id: Int,
val type: String, val type: String,
@SerializedName("local_id") val local_id: Int
val localId: Int
) : Parcelable ) : Parcelable
@Parcelize @Parcelize
data class SortId( data class SortId(
@SerializedName("major_id") val major_id: Int,
val majorId: Int, val minor_id: Int
@SerializedName("minor_id")
val minorId: Int
) : Parcelable ) : Parcelable
@Parcelize @Parcelize
data class PushSettings( data class PushSettings(
@SerializedName("disabled_forever") val disabled_forever: Boolean,
val disabledForever: Boolean, val no_sound: Boolean,
@SerializedName("no_sound") val disabled_mentions: Boolean,
val noSound: Boolean, val disabled_mass_mentions: Boolean
@SerializedName("disabled_mentions")
val disabledMentions: Boolean,
@SerializedName("disabled_mass_mentions")
val disabledMassMentions: Boolean
) : Parcelable ) : Parcelable
@Parcelize @Parcelize
@@ -94,75 +74,50 @@ data class BaseVkConversation(
@Parcelize @Parcelize
data class ChatSettings( data class ChatSettings(
@SerializedName("owner_id") val owner_id: Int,
val ownerId: Int,
val title: String, val title: String,
val state: String, val state: String,
val acl: Acl, val acl: Acl,
@SerializedName("members_count") val members_count: Int,
val membersCount: Int, val friends_count: Int,
@SerializedName("friends_count")
val friendsCount: Int,
val photo: Photo?, val photo: Photo?,
@SerializedName("admin_ids") val admin_ids: List<Int>,
val adminsIds: List<Int>, val active_ids: List<Int>,
@SerializedName("active_ids") val is_group_channel: Boolean,
val activeIds: List<Int>, val is_disappearing: Boolean,
@SerializedName("is_group_channel") val is_service: Boolean,
val isGroupChannel: Boolean,
@SerializedName("is_disappearing")
val isDisappearing: Boolean,
@SerializedName("is_service")
val isService: Boolean,
val theme: String?, val theme: String?,
@SerializedName("pinned_message") val pinned_message: BaseVkMessage?
val pinnedMessage: BaseVkMessage?
) : Parcelable { ) : Parcelable {
@Parcelize @Parcelize
data class Acl( data class Acl(
@SerializedName("can_change_info") val can_change_info: Boolean,
val canChangeInfo: Boolean, val can_change_invite_link: Boolean,
@SerializedName("can_change_invite_link") val can_change_pin: Boolean,
val canChangeInviteLink: Boolean, val can_invite: Boolean,
@SerializedName("can_change_pin") val can_promote_users: Boolean,
val canChangePin: Boolean, val can_see_invite_link: Boolean,
@SerializedName("can_invite") val can_moderate: Boolean,
val canInvite: Boolean, val can_copy_chat: Boolean,
@SerializedName("can_promote_users") val can_call: Boolean,
val canPromoteUsers: Boolean, val can_use_mass_mentions: Boolean,
@SerializedName("can_see_invite_link") val can_change_style: Boolean
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
) : Parcelable ) : Parcelable
@Parcelize @Parcelize
data class Photo( data class Photo(
@SerializedName("photo_50") val photo_50: String?,
val photo50: String?, val photo_100: String?,
@SerializedName("photo_100") val photo_200: String?,
val photo100: String?, val is_default_photo: Boolean
@SerializedName("photo_200")
val photo200: String?,
@SerializedName("is_default_photo")
val isDefaultPhoto: Boolean
) : Parcelable ) : Parcelable
} }
@Parcelize @Parcelize
data class CallInProgress( data class CallInProgress(
val participants: Participants, val participants: BaseVkGroupCall.Participants,
@SerializedName("join_link") val join_link: String
val joinLink: String
) : Parcelable { ) : Parcelable {
@Parcelize @Parcelize
@@ -1,7 +1,6 @@
package com.meloda.fast.api.model.base package com.meloda.fast.api.model.base
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkGroup
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -9,33 +8,24 @@ import kotlinx.parcelize.Parcelize
data class BaseVkGroup( data class BaseVkGroup(
val id: Int, val id: Int,
val name: String, val name: String,
@SerializedName("screen_name") val screen_name: String,
val screenName: String, val is_closed: Int,
@SerializedName("is_closed")
val isClosed: Int,
val type: String, val type: String,
@SerializedName("is_admin") val is_admin: Int,
val isAdmin: Int, val is_member: Int,
@SerializedName("is_member") val is_advertiser: Int,
val isMember: Int, val photo_50: String?,
@SerializedName("is_advertiser") val photo_100: String?,
val isAdvertiser: Int, val photo_200: String?,
@SerializedName("photo_50") val members_count: Int?
val photo50: String?,
@SerializedName("photo_100")
val photo100: String?,
@SerializedName("photo_200")
val photo200: String?,
@SerializedName("members_count")
val membersCount: Int?
) : Parcelable { ) : Parcelable {
fun asVkGroup() = VkGroup( fun asVkGroup() = VkGroup(
id = -id, id = -id,
name = name, name = name,
screenName = screenName, screenName = screen_name,
photo200 = photo200, photo200 = photo_200,
membersCount = membersCount membersCount = members_count
) )
} }
@@ -1,7 +1,6 @@
package com.meloda.fast.api.model.base package com.meloda.fast.api.model.base
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.VkUtils import com.meloda.fast.api.VkUtils
import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
@@ -10,23 +9,17 @@ import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkMessage( data class BaseVkMessage(
val date: Int, val date: Int,
@SerializedName("from_id") val from_id: Int,
val fromId: Int,
val id: Int, val id: Int,
val out: Int, val out: Int,
@SerializedName("peer_id") val peer_id: Int,
val peerId: Int,
val text: String, val text: String,
@SerializedName("conversation_message_id") val conversation_message_id: Int,
val conversationMessageId: Int, val fwd_messages: List<BaseVkMessage>? = listOf(),
@SerializedName("fwd_messages")
val fwdMessages: List<BaseVkMessage>? = listOf(),
val important: Boolean, val important: Boolean,
@SerializedName("random_id") val random_id: Int,
val randomId: Int,
val attachments: List<BaseVkAttachmentItem> = listOf(), val attachments: List<BaseVkAttachmentItem> = listOf(),
@SerializedName("is_hidden") val is_hidden: Boolean,
val isHidden: Boolean,
val payload: String, val payload: String,
val geo: Geo?, val geo: Geo?,
val action: Action?, val action: Action?,
@@ -37,20 +30,20 @@ data class BaseVkMessage(
id = id, id = id,
text = if (text.isBlank()) null else text, text = if (text.isBlank()) null else text,
isOut = out == 1, isOut = out == 1,
peerId = peerId, peerId = peer_id,
fromId = fromId, fromId = from_id,
date = date, date = date,
randomId = randomId, randomId = random_id,
action = action?.type, action = action?.type,
actionMemberId = action?.memberId, actionMemberId = action?.member_id,
actionText = action?.text, actionText = action?.text,
actionConversationMessageId = action?.conversationMessageId, actionConversationMessageId = action?.conversation_message_id,
actionMessage = action?.message, actionMessage = action?.message,
geoType = geo?.type, geoType = geo?.type,
important = important important = important
).also { ).also {
it.attachments = VkUtils.parseAttachments(attachments) it.attachments = VkUtils.parseAttachments(attachments)
it.forwards = VkUtils.parseForwards(fwdMessages) it.forwards = VkUtils.parseForwards(fwd_messages)
} }
@Parcelize @Parcelize
@@ -71,11 +64,9 @@ data class BaseVkMessage(
@Parcelize @Parcelize
data class Action( data class Action(
val type: String, val type: String,
@SerializedName("member_id") val member_id: Int?,
val memberId: Int?,
val text: String?, val text: String?,
@SerializedName("conversation_message_id") val conversation_message_id: Int?,
val conversationMessageId: Int?,
val message: String? val message: String?
) : Parcelable ) : Parcelable
@@ -1,35 +1,24 @@
package com.meloda.fast.api.model.base package com.meloda.fast.api.model.base
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.VkUser
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkUser( data class BaseVkUser(
val id: Int, val id: Int,
@SerializedName("first_name") val first_name: String,
val firstName: String, val last_name: String,
@SerializedName("last_name") val can_access_closed: Boolean,
val lastName: String, val is_closed: Boolean,
@SerializedName("can_access_closed") val can_invite_to_chats: Boolean,
val canAccessClosed: Boolean,
@SerializedName("is_closed")
val isClosed: Boolean,
@SerializedName("can_invite_to_chats")
val canInviteToChats: Boolean,
val sex: Int?, val sex: Int?,
@SerializedName("photo_50") val photo_50: String?,
val photo50: String?, val photo_100: String?,
@SerializedName("photo_100") val photo_200: String?,
val photo100: String?,
@SerializedName("photo_200")
val photo200: String?,
val online: Int?, val online: Int?,
@SerializedName("online_info") val online_info: OnlineInfo?,
val onlineInfo: OnlineInfo?, val screen_name: String
@SerializedName("screen_name")
val screenName: String
//...other fields //...other fields
) : Parcelable { ) : Parcelable {
@@ -37,24 +26,20 @@ data class BaseVkUser(
data class OnlineInfo( data class OnlineInfo(
val visible: Boolean, val visible: Boolean,
val status: String, val status: String,
@SerializedName("last_seen") val last_seen: Int?,
val lastSeen: Int?, val is_online: Boolean?,
@SerializedName("is_online") val online_mobile: Boolean?,
val isOnline: Boolean?, val app_id: Int?
@SerializedName("online_mobile")
val isOnlineMobile: Boolean?,
@SerializedName("app_id")
val appId: Int?
) : Parcelable ) : Parcelable
fun asVkUser() = VkUser( fun asVkUser() = VkUser(
id = id, id = id,
firstName = firstName, firstName = first_name,
lastName = lastName, lastName = last_name,
online = online == 1, online = online == 1,
photo200 = photo200, photo200 = photo_200,
lastSeen = onlineInfo?.lastSeen, lastSeen = online_info?.last_seen,
lastSeenStatus = onlineInfo?.status lastSeenStatus = online_info?.status
) )
} }
@@ -1,6 +1,7 @@
package com.meloda.fast.api.model.base.attachments package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import android.util.Log
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@@ -26,12 +27,16 @@ data class BaseVkAttachmentItem(
val wallReply: BaseVkWallReply?, val wallReply: BaseVkWallReply?,
val call: BaseVkCall?, val call: BaseVkCall?,
@SerializedName("group_call_in_progress") @SerializedName("group_call_in_progress")
val groupCall: BaseVkGroupCall? val groupCall: BaseVkGroupCall?,
val curator: BaseVkCurator?,
val event: BaseVkEvent?,
val story: BaseVkStory?
) : Parcelable { ) : Parcelable {
fun getPreparedType() = AttachmentType.parse(type) fun getPreparedType() = AttachmentType.parse(type)
enum class AttachmentType(val value: String) { enum class AttachmentType(var value: String) {
UNKNOWN("unknown"),
PHOTO("photo"), PHOTO("photo"),
VIDEO("video"), VIDEO("video"),
AUDIO("audio"), AUDIO("audio"),
@@ -46,11 +51,22 @@ data class BaseVkAttachmentItem(
POLL("poll"), POLL("poll"),
WALL_REPLY("wall_reply"), WALL_REPLY("wall_reply"),
CALL("call"), 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 { 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 package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName import com.meloda.fast.api.model.attachments.VkAudio
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -12,37 +12,33 @@ data class BaseVkAudio(
val duration: Int, val duration: Int,
val url: String, val url: String,
val date: Int, val date: Int,
@SerializedName("owner_id") val owner_id: Int,
val ownerId: Int, val access_key: String,
@SerializedName("access_key") val is_explicit: Boolean,
val accessKey: String, val is_focus_track: Boolean,
@SerializedName("is_explicit") val is_licensed: Boolean,
val isExplicit: Boolean, val track_code: String,
@SerializedName("is_focus_track") val genre_id: Int,
val isFocusTrack: Boolean,
@SerializedName("is_licensed")
val isLicensed: Boolean,
@SerializedName("track_code")
val trackCode: String,
@SerializedName("genre_id")
val genreId: Int,
val album: Album, val album: Album,
@SerializedName("short_videos_allowed") val short_videos_allowed: Boolean,
val shortVideosAllowed: Boolean, val stories_allowed: Boolean,
@SerializedName("stories_allowed") val stories_cover_allowed: Boolean
val storiesAllowed: Boolean,
@SerializedName("stories_cover_allowed")
val storiesCoverAllowed: Boolean
) : BaseVkAttachment() { ) : BaseVkAttachment() {
fun asVkAudio() = VkAudio(
id = id,
title = title,
artist = artist,
url = url,
duration = duration
)
@Parcelize @Parcelize
data class Album( data class Album(
val id: Int, val id: Int,
val title: String, val title: String,
@SerializedName("owner_id") val owner_id: Int,
val ownerId: Int, val access_key: String,
@SerializedName("access_key")
val accessKey: String,
val thumb: Thumb val thumb: Thumb
) : Parcelable { ) : Parcelable {
@@ -50,22 +46,13 @@ data class BaseVkAudio(
data class Thumb( data class Thumb(
val width: Int, val width: Int,
val height: Int, val height: Int,
@SerializedName("photo_34") val photo_34: String,
val photo34: String, val photo_68: String,
@SerializedName("photo_68") val photo_135: String,
val photo68: String, val photo_270: String,
@SerializedName("photo_135") val photo_300: String,
val photo135: String, val photo_600: String,
@SerializedName("photo_270") val photo_1200: String
val photo270: String,
@SerializedName("photo_300")
val photo300: String,
@SerializedName("photo_600")
val photo600: String,
@SerializedName("photo_1200")
val photo1200: String
) : Parcelable ) : Parcelable
} }
} }
@@ -1,15 +1,12 @@
package com.meloda.fast.api.model.base.attachments package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkCall( data class BaseVkCall(
@SerializedName("initiator_id") val initiator_id: Int,
val initiatorId: Int, val receiver_id: Int,
@SerializedName("receiver_id")
val receiverId: Int,
val state: String, val state: String,
val time: Int, val time: Int,
val duration: 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 package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName import com.meloda.fast.api.model.attachments.VkFile
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkFile( data class BaseVkFile(
val id: Int, val id: Int,
@SerializedName("owner_id") val owner_id: Int,
val ownerId: Int,
val title: String, val title: String,
val size: Int, val size: Int,
val ext: String, val ext: String,
@@ -16,14 +15,19 @@ data class BaseVkFile(
val type: Int, val type: Int,
val url: String, val url: String,
val preview: Preview?, val preview: Preview?,
@SerializedName("is_licensed") val ic_licensed: Int,
val isLicensed: Int, val access_key: String,
@SerializedName("access_key") val web_preview_url: String?
val accessKey: String,
@SerializedName("web_preview_url")
val webPreviewUrl: String?
) : BaseVkAttachment() { ) : BaseVkAttachment() {
fun asVkFile() = VkFile(
id = id,
title = title,
ext = ext,
url = url,
size = size
)
@Parcelize @Parcelize
data class Preview( data class Preview(
val photo: Photo?, val photo: Photo?,
@@ -31,15 +35,24 @@ data class BaseVkFile(
) : Parcelable { ) : Parcelable {
@Parcelize @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 @Parcelize
data class Video( data class Video(
val src: String, val src: String,
val width: Int, val width: Int,
val height: Int, val height: Int,
@SerializedName("file_size") val file_size: Int
val fileSize: Int
) : Parcelable ) : Parcelable
} }
@@ -1,16 +1,12 @@
package com.meloda.fast.api.model.base.attachments package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkGift( data class BaseVkGift(
val id: Int, val id: Int,
@SerializedName("thumb_256") val thumb_256: String?,
val thumb256: String?, val thumb_96: String?,
@SerializedName("thumb_96") val thumb_48: String
val thumb96: String?,
@SerializedName("thumb_48")
val thumb48: String
) : Parcelable ) : Parcelable
@@ -1,17 +1,14 @@
package com.meloda.fast.api.model.base.attachments package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkGraffiti( data class BaseVkGraffiti(
val id: Int, val id: Int,
@SerializedName("owner_id") val owner_id: Int,
val ownerId: Int,
val url: String, val url: String,
val width: Int, val width: Int,
val height: Int, val height: Int,
@SerializedName("access_key") val access_key: String
val accessKey: String
) : Parcelable ) : Parcelable
@@ -1,15 +1,12 @@
package com.meloda.fast.api.model.base.attachments package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkGroupCall( data class BaseVkGroupCall(
@SerializedName("initiator_id") val initiator_id: Int,
val initiatorId: Int, val join_link: String,
@SerializedName("join_link")
val joinLink: String,
val participants: Participants val participants: Participants
) : Parcelable { ) : Parcelable {
@@ -1,15 +1,25 @@
package com.meloda.fast.api.model.base.attachments 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 import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkLink( data class BaseVkLink(
val url: String, val url: String,
val title: String, val title: String?,
val caption: String, val caption: String?,
val photo: BaseVkPhoto, val photo: BaseVkPhoto?,
val target: String, val target: String,
@SerializedName("is_favorite") val is_favorite: Boolean
val isFavorite: Boolean ) : BaseVkAttachment() {
) : 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 description: String,
val app: App, val app: App,
val images: List<Image>?, val images: List<Image>?,
@SerializedName("button_text") val button_text: String
val buttonText: String
) : Parcelable { ) : Parcelable {
@Parcelize @Parcelize
@@ -1,47 +1,43 @@
package com.meloda.fast.api.model.base.attachments package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.attachments.VkPhoto import com.meloda.fast.api.model.attachments.VkPhoto
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkPhoto( data class BaseVkPhoto(
@SerializedName("album_id") val album_id: Int,
val albumId: Int,
val date: Int, val date: Int,
val id: Int, val id: Int,
@SerializedName("owner_id") val owner_id: Int,
val ownerId: Int, val has_tags: Boolean,
@SerializedName("has_tags") val access_key: String?,
val hasTags: Boolean,
@SerializedName("access_key")
val accessKey: String?,
val sizes: List<Size>, val sizes: List<Size>,
val text: String, val text: String,
@SerializedName("user_id") val user_id: Int?,
val userId: Int? val lat: Double?,
val long: Double?,
val post_id: Int?
) : BaseVkAttachment() { ) : BaseVkAttachment() {
fun asVkPhoto() = VkPhoto( fun asVkPhoto() = VkPhoto(
albumId = albumId, albumId = album_id,
date = date, date = date,
id = id, id = id,
ownerId = ownerId, ownerId = owner_id,
hasTags = hasTags, hasTags = has_tags,
accessKey = accessKey, accessKey = access_key,
sizes = sizes, sizes = sizes,
text = text, 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 package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -11,30 +10,20 @@ data class BaseVkPoll(
val votes: Int, val votes: Int,
val anonymous: Boolean, val anonymous: Boolean,
val closed: Boolean, val closed: Boolean,
@SerializedName("end_date") val end_date: Int,
val endDate: Int, val is_board: Boolean,
@SerializedName("is_board") val can_vote: Boolean,
val isBoard: Boolean, val can_edit: Boolean,
@SerializedName("can_vote") val can_report: Boolean,
val canVote: Boolean, val can_share: Boolean,
@SerializedName("can_edit")
val canEdit: Boolean,
@SerializedName("can_report")
val canReport: Boolean,
@SerializedName("can_share")
val canShare: Boolean,
val created: Int, val created: Int,
@SerializedName("owner_id") val owner_id: Int,
val ownerId: Int,
val question: String, val question: String,
@SerializedName("disable_unvote") val disable_unvote: Boolean,
val disableUnVote: Boolean,
val friends: List<Friend>?, val friends: List<Friend>?,
@SerializedName("embed_hash") val embed_hash: String,
val embedHash: String,
val answers: List<Answer>, val answers: List<Answer>,
@SerializedName("author_id") val author_id: Int,
val authorId: Int,
val background: Background? val background: Background?
) : Parcelable { ) : Parcelable {
@@ -1,30 +1,24 @@
package com.meloda.fast.api.model.base.attachments package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.IntDef
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.attachments.VkSticker import com.meloda.fast.api.model.attachments.VkSticker
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkSticker( data class BaseVkSticker(
@SerializedName("product_id") val product_id: Int,
val productId: Int, val sticker_id: Int,
@SerializedName("sticker_id")
val stickerId: Int,
val images: List<Image>, val images: List<Image>,
@SerializedName("images_with_background") val images_with_background: List<Image>,
val imagesWithBackground: List<Image>, val animation_url: String?,
@SerializedName("animation_url")
val animationUrl: String?,
val animations: List<Animation>? val animations: List<Animation>?
) : Parcelable { ) : Parcelable {
fun asVkSticker() = VkSticker( fun asVkSticker() = VkSticker(
id = stickerId, id = sticker_id,
productId = productId, productId = product_id,
images = images, images = images,
backgroundImages = imagesWithBackground backgroundImages = images_with_background
) )
@Parcelize @Parcelize
@@ -41,6 +35,3 @@ data class BaseVkSticker(
) : Parcelable ) : 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 package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import com.meloda.fast.api.model.attachments.VkVideo import com.meloda.fast.api.model.attachments.VkVideo
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
//not all fields
@Parcelize @Parcelize
data class BaseVkVideo( data class BaseVkVideo(
val id: Int, val id: Int,
@@ -20,45 +18,30 @@ data class BaseVkVideo(
val added: Int, val added: Int,
val type: String, val type: String,
val views: Int, val views: Int,
@SerializedName("can_comment") val can_comment: Int,
val canComment: Int, val can_edit: Int,
@SerializedName("can_edit") val can_like: Int,
val canEdit: Int, val can_repost: Int,
@SerializedName("can_like") val can_subscribe: Int,
val canLike: Int, val can_add_to_faves: Int,
@SerializedName("can_repost") val can_add: Int,
val canRepost: Int, val can_attach_link: Int,
@SerializedName("can_subscribe") val access_key: String,
val canSubscribe: Int, val owner_id: Int,
@SerializedName("can_add_to_faves") val ov_id: String,
val canAddToFaves: Int, val is_favorite: Boolean,
@SerializedName("can_add") val track_code: String,
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 image: List<Image>, val image: List<Image>,
@SerializedName("first_frame") val first_frame: List<FirstFrame>,
val firstFrame: List<FirstFrame>,
val files: File, val files: File,
@SerializedName("timeline_thumbs") val timeline_thumbs: TimelineThumbs,
val timelineThumbs: TimelineThumbs val ads: Ads
//ads
) : BaseVkAttachment() { ) : BaseVkAttachment() {
fun asVkVideo() = VkVideo( fun asVkVideo() = VkVideo(
id = id, id = id,
images = image, images = image,
firstFrames = firstFrame firstFrames = first_frame
) )
@Parcelize @Parcelize
@@ -66,8 +49,7 @@ data class BaseVkVideo(
val height: Int, val height: Int,
val width: Int, val width: Int,
val url: String, val url: String,
@SerializedName("with_padding") val with_padding: Int
val withPadding: Int
) : Parcelable ) : Parcelable
@Parcelize @Parcelize
@@ -86,34 +68,62 @@ data class BaseVkVideo(
val mp4_1080: String?, val mp4_1080: String?,
val mp4_1440: String?, val mp4_1440: String?,
val hls: String, val hls: String,
@SerializedName("dash_uni") val dash_uni: String,
val dashUni: String, val dash_sep: String,
@SerializedName("dash_sep") val hls_ondemand: String,
val dashSep: String, val dash_ondemand: String,
@SerializedName("hls_ondemand") val failover_host: String
val hlsOnDemand: String,
@SerializedName("dash_ondemand")
val dashOnDemand: String,
@SerializedName("failover_host")
val failOverHost: String
) : Parcelable ) : Parcelable
@Parcelize @Parcelize
data class TimelineThumbs( data class TimelineThumbs(
@SerializedName("count_per_image") val count_per_image: Int,
val countPerImage: Int, val count_per_row: Int,
@SerializedName("count_per_row") val count_total: Int,
val countPerRow: Int, val frame_height: Int,
@SerializedName("count_total") val frame_width: Float,
val countTotal: Int,
@SerializedName("frame_height")
val frameHeight: Int,
@SerializedName("frame_width")
val frameWidth: Float,
val links: List<String>, val links: List<String>,
@SerializedName("is_uv") val is_uv: Boolean,
val isUv: Boolean,
val frequency: Int val frequency: Int
) : Parcelable ) : 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 package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkVoiceMessage( data class BaseVkVoiceMessage(
val id: Int, val id: Int,
@SerializedName("owner_id") val owner_id: Int,
val ownerId: Int,
val duration: Int, val duration: Int,
val waveform: List<Int>, val waveform: List<Int>,
@SerializedName("link_ogg") val link_ogg: String,
val linkOgg: String, val link_mp3: String,
@SerializedName("link_mp3") val access_key: String,
val linkMp3: String, val transcript_state: String,
@SerializedName("access_key")
val accessKey: String,
@SerializedName("transcript_state")
val transcriptState: String,
val transcript: String val transcript: String
) : Parcelable ) : Parcelable
@@ -1,34 +1,43 @@
package com.meloda.fast.api.model.base.attachments package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName import com.meloda.fast.api.model.attachments.VkWall
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkWall( data class BaseVkWall(
val id: Int, val id: Int,
@SerializedName("from_id") val from_id: Int,
val fromId: Int, val to_id: Int,
@SerializedName("to_id")
val toId: Int,
val date: Int, val date: Int,
val text: String, val text: String,
val attachments: List<BaseVkAttachmentItem>?, val attachments: List<BaseVkAttachmentItem>?,
@SerializedName("post_source") val post_source: PostSource,
val postSource: PostSource,
val comments: Comments, val comments: Comments,
val likes: Likes, val likes: Likes,
val reposts: Reposts, val reposts: Reposts,
val views: Views, val views: Views,
@SerializedName("is_favorite") val is_favorite: Boolean,
val isFavorite: Boolean,
val donut: Donut, val donut: Donut,
@SerializedName("access_key") val access_key: String,
val accessKey: String, val short_text_rate: Double
@SerializedName("short_text_rate")
val shortTextRate: Double
) : Parcelable { ) : 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 @Parcelize
data class PostSource( data class PostSource(
val type: String, val type: String,
@@ -38,28 +47,22 @@ data class BaseVkWall(
@Parcelize @Parcelize
data class Comments( data class Comments(
val count: Int, val count: Int,
@SerializedName("can_post") val can_post: Int,
val canPost: Int, val groups_can_post: Boolean
@SerializedName("groups_can_post")
val groupsCanPost: Boolean
) : Parcelable ) : Parcelable
@Parcelize @Parcelize
data class Likes( data class Likes(
val count: Int, val count: Int,
@SerializedName("user_likes") val user_likes: Int,
val userLikes: Int, val can_like: Int,
@SerializedName("can_like") val can_publish: Int,
val canLike: Int,
@SerializedName("can_publish")
val canPublish: Int,
) : Parcelable ) : Parcelable
@Parcelize @Parcelize
data class Reposts( data class Reposts(
val count: Int, val count: Int,
@SerializedName("user_reposted") val user_reposted: Int
val userReposted: Int
) : Parcelable ) : Parcelable
@Parcelize @Parcelize
@@ -69,8 +72,7 @@ data class BaseVkWall(
@Parcelize @Parcelize
data class Donut( data class Donut(
@SerializedName("is_donut") val is_donut: Boolean
val isDonut: Boolean
) : Parcelable ) : Parcelable
} }
@@ -1,39 +1,29 @@
package com.meloda.fast.api.model.base.attachments package com.meloda.fast.api.model.base.attachments
import android.os.Parcelable import android.os.Parcelable
import com.google.gson.annotations.SerializedName
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class BaseVkWallReply( data class BaseVkWallReply(
val id: Int, val id: Int,
@SerializedName("from_id") val from_id: Int,
val fromId: Int,
val date: Int, val date: Int,
val text: String, val text: String,
@SerializedName("post_id") val post_id: Int,
val postId: Int, val owner_id: Int,
@SerializedName("owner_id") val parents_stack: List<Int>,
val ownerId: Int,
@SerializedName("parents_stack")
val parentsStack: List<Int>,
val likes: Likes, val likes: Likes,
@SerializedName("reply_to_user") val reply_to_user: Int?,
val replyToUser: Int?, val reply_to_comment: Int?
@SerializedName("reply_to_comment")
val replyToComment: Int?
) : Parcelable { ) : Parcelable {
@Parcelize @Parcelize
data class Likes( data class Likes(
val count: Int, val count: Int,
@SerializedName("can_like") val can_like: Int,
val canLike: Int, val user_likes: Int,
@SerializedName("user_likes") val can_publish: Int
val userLikes: Int,
@SerializedName("can_publish")
val canPublish: Int
) : Parcelable ) : Parcelable
} }
@@ -77,18 +77,21 @@ internal class ResultCall<T>(proxy: Call<T>) : CallDelegate<T, Answer<T>>(proxy)
) : Callback<T> { ) : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) { override fun onResponse(call: Call<T>, response: Response<T>) {
var isVkException = true
val result: Answer<T> = val result: Answer<T> =
if (response.isSuccessful) { if (response.isSuccessful) {
val baseBody = response.body() val baseBody = response.body()
if (baseBody !is ApiResponse<*>) Answer.Success(baseBody as T) if (baseBody !is ApiResponse<*>) Answer.Success(baseBody as T)
else { else {
val body = baseBody as ApiResponse<*> val body = baseBody as ApiResponse<*>
if (body.error != null) Answer.Error(body.error) if (body.error != null) {
else Answer.Success(body as T) Answer.Error(body.error)
} else Answer.Success(body as T)
} }
} else Answer.Error(IOException(response.errorBody()?.string() ?: "")) } 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)) callback.onResponse(proxy, Response.success(result))
@@ -7,15 +7,16 @@ import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.util.Log
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.room.Room import androidx.room.Room
import com.meloda.fast.BuildConfig import com.meloda.fast.BuildConfig
import com.meloda.fast.database.AppDatabase import com.meloda.fast.database.AppDatabase
import com.meloda.fast.util.AndroidUtils
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import org.acra.ACRA import org.acra.ACRA
import kotlin.math.sqrt
@HiltAndroidApp @HiltAndroidApp
class AppGlobal : Application() { class AppGlobal : Application() {
@@ -66,8 +67,24 @@ class AppGlobal : Application() {
Companion.packageName = packageName Companion.packageName = packageName
Companion.packageManager = packageManager Companion.packageManager = packageManager
screenWidth = AndroidUtils.getDisplayWidth() screenWidth = resources.displayMetrics.widthPixels
screenHeight = AndroidUtils.getDisplayHeight() 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 inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 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.content.res.ColorStateList
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.text.SpannableString import android.text.SpannableString
import android.text.TextUtils
import android.text.style.ForegroundColorSpan import android.text.style.ForegroundColorSpan
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@@ -27,7 +28,8 @@ class ConversationsAdapter constructor(
context: Context, context: Context,
values: MutableList<VkConversation>, values: MutableList<VkConversation>,
val profiles: HashMap<Int, VkUser> = hashMapOf(), 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>( ) : BaseAdapter<VkConversation, ConversationsAdapter.ItemHolder>(
context, values, COMPARATOR context, values, COMPARATOR
) { ) {
@@ -41,6 +43,11 @@ class ConversationsAdapter constructor(
private val dateColor = ContextCompat.getColor(context, R.color.n2_500) private val dateColor = ContextCompat.getColor(context, R.color.n2_500)
private val youPrefix = context.getString(R.string.you_message_prefix) 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) { override fun bind(position: Int) {
val conversation = getItem(position) val conversation = getItem(position)
@@ -48,6 +55,11 @@ class ConversationsAdapter constructor(
binding.callIcon.isVisible = conversation.callInProgress binding.callIcon.isVisible = conversation.callInProgress
binding.phantomIcon.isVisible = conversation.isPhantom 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!! val message = if (conversation.lastMessage != null) conversation.lastMessage!!
else { else {
binding.title.text = conversation.title binding.title.text = conversation.title
@@ -129,6 +141,7 @@ class ConversationsAdapter constructor(
binding.pin.isVisible = conversation.isPinned binding.pin.isVisible = conversation.isPinned
val actionMessage = VkUtils.getActionConversationText( val actionMessage = VkUtils.getActionConversationText(
context = context,
message = message, message = message,
youPrefix = youPrefix, youPrefix = youPrefix,
profiles = profiles, profiles = profiles,
@@ -6,10 +6,13 @@ import android.viewbinding.library.fragment.viewBinding
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.datastore.preferences.core.edit
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import coil.load import coil.load
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.snackbar.Snackbar
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.api.UserConfig import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.model.VkConversation 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.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent 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.databinding.FragmentConversationsBinding
import com.meloda.fast.util.AndroidUtils import com.meloda.fast.util.AndroidUtils
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlin.math.abs import kotlin.math.abs
@AndroidEntryPoint @AndroidEntryPoint
@@ -59,7 +67,18 @@ class ConversationsFragment :
binding.recyclerView.adapter = adapter 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) { UserConfig.vkUser.observe(viewLifecycleOwner) {
it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } } it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } }
@@ -87,6 +106,18 @@ class ConversationsFragment :
viewModel.loadProfileUser() viewModel.loadProfileUser()
viewModel.loadConversations() 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) { 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.content.Context
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.view.Gravity
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast
import androidx.appcompat.widget.LinearLayoutCompat import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
import coil.load 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.VkMessage
import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.VkUser
import com.meloda.fast.api.model.attachments.VkPhoto import com.meloda.fast.api.model.attachments.VkPhoto
import com.meloda.fast.api.model.attachments.VkSticker
import com.meloda.fast.api.model.attachments.VkVideo
import com.meloda.fast.base.adapter.BaseAdapter import com.meloda.fast.base.adapter.BaseAdapter
import com.meloda.fast.base.adapter.BaseHolder import com.meloda.fast.base.adapter.BaseHolder
import com.meloda.fast.databinding.* import com.meloda.fast.databinding.*
import com.meloda.fast.util.AndroidUtils import com.meloda.fast.util.AndroidUtils
import java.text.SimpleDateFormat
import java.util.* import java.util.*
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -45,21 +39,6 @@ class MessagesHistoryAdapter constructor(
getItem(position).let { message -> getItem(position).let { message ->
if (message.action != null) return SERVICE 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 OUTGOING
if (!message.isOut) return INCOMING if (!message.isOut) return INCOMING
} }
@@ -72,26 +51,12 @@ class MessagesHistoryAdapter constructor(
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
return when (viewType) { return when (viewType) {
// magick numbers is great!
HEADER -> Header(createEmptyView(60)) HEADER -> Header(createEmptyView(60))
FOOTER -> Footer(createEmptyView(36)) FOOTER -> Footer(createEmptyView(36))
SERVICE -> ServiceMessage( SERVICE -> ServiceMessage(
ItemMessageServiceBinding.inflate(inflater, parent, false) 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( OUTGOING -> OutgoingMessage(
ItemMessageOutBinding.inflate(inflater, parent, false) ItemMessageOutBinding.inflate(inflater, parent, false)
) )
@@ -130,81 +95,31 @@ class MessagesHistoryAdapter constructor(
private val binding: ItemMessageInBinding private val binding: ItemMessageInBinding
) : Holder(binding.root) { ) : 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) { override fun bind(position: Int) {
val message = getItem(position) val message = getItem(position)
val prevMessage = getOrNull(position - 1) val prevMessage = getOrNull(position - 1)
val nextMessage = getOrNull(position + 1) val nextMessage = getOrNull(position + 1)
binding.unread.isVisible = message.isRead(conversation) MessagesPreparator(
context = context,
binding.bubble.background = conversation = conversation,
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(
message = message, message = message,
messageUser = messageUser, prevMessage = prevMessage,
messageGroup = messageGroup, nextMessage = nextMessage,
imageView = binding.avatar
)
val title = when { avatar = binding.avatar,
message.isUser() && messageUser != null -> messageUser.firstName bubble = binding.bubble,
message.isGroup() && messageGroup != null -> messageGroup.name text = binding.text,
else -> null spacer = binding.spacer,
} time = binding.time,
unread = binding.unread,
attachmentContainer = binding.attachmentContainer,
attachmentSpacer = binding.attachmentSpacer,
binding.title.text = title profiles = profiles,
binding.title.measure(0, 0) groups = groups
).prepare()
if (binding.title.isVisible) {
binding.bubble.minimumWidth = binding.title.measuredWidth + 60
} else {
binding.bubble.minimumWidth = 0
}
MessagesManager.setMessageText(
message = message,
textView = binding.text
)
} }
} }
@@ -212,23 +127,8 @@ class MessagesHistoryAdapter constructor(
private val binding: ItemMessageOutBinding private val binding: ItemMessageOutBinding
) : Holder(binding.root) { ) : 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 { init {
MessagesManager.setRootMaxWidth(binding.bubble)
binding.bubbleStroke.setOnClickListener { binding.bubble.performClick() } binding.bubbleStroke.setOnClickListener { binding.bubble.performClick() }
binding.bubble.setOnClickListener {
binding.time.isVisible = !binding.time.isVisible
}
} }
override fun bind(position: Int) { override fun bind(position: Int) {
@@ -236,27 +136,25 @@ class MessagesHistoryAdapter constructor(
val prevMessage = getOrNull(position - 1) 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 = profiles = profiles,
!(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) groups = groups
).prepare()
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)
} }
} }
inner class ServiceMessage( inner class ServiceMessage(
@@ -285,6 +183,7 @@ class MessagesHistoryAdapter constructor(
message.action ?: return message.action ?: return
binding.message.text = VkUtils.getActionMessageText( binding.message.text = VkUtils.getActionMessageText(
context = context,
message = message, message = message,
youPrefix = youPrefix, youPrefix = youPrefix,
profiles = profiles, 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 private val actualSize get() = values.size
override fun getItemCount(): Int { override fun getItemCount(): Int {
@@ -556,13 +228,6 @@ class MessagesHistoryAdapter constructor(
private const val OUTGOING = 4 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>() { private val COMPARATOR = object : DiffUtil.ItemCallback<VkMessage>() {
override fun areItemsTheSame( override fun areItemsTheSame(
oldItem: VkMessage, oldItem: VkMessage,
@@ -3,6 +3,7 @@ package com.meloda.fast.screens.messages
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.os.Bundle import android.os.Bundle
import android.text.TextUtils
import android.view.View import android.view.View
import android.viewbinding.library.fragment.viewBinding import android.viewbinding.library.fragment.viewBinding
import androidx.core.view.isVisible import androidx.core.view.isVisible
@@ -76,6 +77,9 @@ class MessagesHistoryFragment :
else -> null else -> null
} }
binding.title.ellipsize = TextUtils.TruncateAt.END
binding.status.ellipsize = TextUtils.TruncateAt.END
binding.title.text = title ?: "..." binding.title.text = title ?: "..."
val status = when { val status = when {
@@ -64,7 +64,7 @@ class MessagesHistoryViewModel @Inject constructor(
response.conversations?.let { baseConversations -> response.conversations?.let { baseConversations ->
baseConversations.forEach { baseConversation -> baseConversations.forEach { baseConversation ->
baseConversation.asVkConversation( baseConversation.asVkConversation(
messages[baseConversation.lastMessageId] messages[baseConversation.last_message_id]
).let { conversation -> conversations[conversation.id] = conversation } ).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 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:id="@+id/title"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:singleLine="true" android:maxLines="1"
android:textColor="@color/n1_900" android:textColor="@color/n1_900"
android:textSize="24sp" android:textSize="24sp"
tools:text="@tools:sample/full_names" /> tools:text="@tools:sample/full_names" />
@@ -154,6 +154,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:alpha="0.7" android:alpha="0.7"
android:maxLines="1"
android:textColor="@color/n1_900" android:textColor="@color/n1_900"
tools:text="Online" /> 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:id="@+id/spacer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="10dp" android:layout_height="10dp"
android:visibility="gone" /> android:visibility="gone"
tools:visibility="visible" />
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
android:id="@+id/title" android:id="@+id/title"
@@ -44,12 +45,14 @@
android:gravity="bottom" android:gravity="bottom"
android:orientation="horizontal"> android:orientation="horizontal">
<com.meloda.fast.widget.BoundedFrameLayout <com.meloda.fast.widget.BoundedLinearLayout
android:id="@+id/bubble" android:id="@+id/bubble"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/ic_message_in_background" android:background="@drawable/ic_message_in_background"
android:backgroundTint="@color/n2_100" android:backgroundTint="@color/n2_100"
android:orientation="vertical"
android:padding="15dp"
tools:ignore="UselessParent"> tools:ignore="UselessParent">
<com.google.android.material.textview.MaterialTextView <com.google.android.material.textview.MaterialTextView
@@ -57,11 +60,24 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:padding="15dp" android:autoLink="all"
android:textColor="@color/n1_800" android:textColor="@color/n1_800"
tools:text="This" /> 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 <com.meloda.fast.widget.CircleImageView
android:id="@+id/unread" android:id="@+id/unread"
@@ -72,6 +88,19 @@
android:src="@color/a3_200" /> android:src="@color/a3_200" />
</androidx.appcompat.widget.LinearLayoutCompat> </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>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout> </layout>
+21 -9
View File
@@ -38,25 +38,38 @@
android:padding="1.5dp" android:padding="1.5dp"
tools:ignore="UselessParent"> tools:ignore="UselessParent">
<com.meloda.fast.widget.BoundedFrameLayout <com.meloda.fast.widget.BoundedLinearLayout
android:id="@+id/bubble" android:id="@+id/bubble"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" 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 <com.google.android.material.textview.MaterialTextView
android:id="@+id/text" android:id="@+id/text"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start" android:layout_gravity="center_vertical|start"
android:padding="15dp"
android:textColor="@color/n1_900" android:textColor="@color/n1_900"
tools:text="This is test" /> tools:text="This is test" />
</com.meloda.fast.widget.BoundedFrameLayout> <Space
</FrameLayout> 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 <com.google.android.material.textview.MaterialTextView
android:id="@+id/time" android:id="@+id/time"
@@ -65,12 +78,11 @@
android:layout_gravity="end" android:layout_gravity="end"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:textColor="?textColorSecondaryVariant" android:textColor="?textColorSecondaryVariant"
android:visibility="gone"
tools:layout_height="18dp" tools:layout_height="18dp"
tools:text="12:00" /> tools:text="12:00"
tools:visibility="visible" />
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat> </androidx.appcompat.widget.LinearLayoutCompat>
</layout> </layout>
+7
View File
@@ -25,6 +25,13 @@
<attr name="bounded_height" format="dimension" /> <attr name="bounded_height" format="dimension" />
</declare-styleable> </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"> <declare-styleable name="WrapTextView">
<attr name="fixWrap" format="boolean" /> <attr name="fixWrap" format="boolean" />
</declare-styleable> </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="week_short">W</string>
<string name="day_short">D</string> <string name="day_short">D</string>
<string name="time_now">Now</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_login_hint">Input login</string>
<string name="input_password_hint">Input password</string> <string name="input_password_hint">Input password</string>
<string name="input_code_hint">Input code</string> <string name="input_code_hint">Input code</string>
@@ -51,4 +51,62 @@
<string name="unknown_error_occurred">Unknown error occurred</string> <string name="unknown_error_occurred">Unknown error occurred</string>
<string name="authorization_failed">Authorization failed</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> </resources>