diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f29f3a69..0117e214 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,9 +7,9 @@ plugins { id("com.android.application") id("kotlin-android") id("kotlin-kapt") + id("kotlin-parcelize") id("androidx.navigation.safeargs.kotlin") id("dagger.hilt.android.plugin") - id("kotlin-parcelize") } android { @@ -38,6 +38,9 @@ android { getByName("release") { isMinifyEnabled = false + buildConfigField("String", "vkLogin", login) + buildConfigField("String", "vkPassword", password) + proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" @@ -68,7 +71,7 @@ android { kapt { correctErrorTypes = true - //use this shit if you don't want to have hilt errors + //use this shit if you don't want have hilt errors javacOptions { option("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true") } @@ -79,13 +82,19 @@ dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") + implementation("com.github.massoudss:waveformSeekBar:3.1.0") + + implementation("androidx.core:core-splashscreen:1.0.0-alpha02") + 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("com.google.android.material:material:1.5.0-alpha03") - implementation("androidx.core:core-ktx:1.7.0-beta01") + implementation("androidx.paging:paging-runtime-ktx:3.0.1") + + implementation("androidx.appcompat:appcompat:1.4.0-beta01") + implementation("com.google.android.material:material:1.5.0-alpha04") + implementation("androidx.core:core-ktx:1.7.0-beta02") implementation("androidx.preference:preference-ktx:1.1.1") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01") implementation("androidx.recyclerview:recyclerview:1.2.1") @@ -93,7 +102,7 @@ dependencies { implementation("androidx.fragment:fragment-ktx:1.3.6") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2-native-mt") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") implementation("androidx.room:room-ktx:2.3.0") implementation("androidx.room:room-runtime:2.3.0") @@ -113,16 +122,16 @@ dependencies { implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") - implementation("com.google.dagger:hilt-android:2.38.1") - kapt("com.google.dagger:hilt-android-compiler:2.38.1") + implementation("com.google.dagger:hilt-android:2.39.1") + kapt("com.google.dagger:hilt-android-compiler:2.39.1") implementation("androidx.hilt:hilt-navigation-fragment:1.0.0") implementation("com.github.yogacp:android-viewbinding:1.0.3") - implementation("io.coil-kt:coil:1.3.2") + implementation("io.coil-kt:coil:1.4.0") implementation("com.google.code.gson:gson:2.8.8") - implementation("org.jsoup:jsoup:1.14.2") + implementation("org.jsoup:jsoup:1.14.3") implementation("ch.acra:acra:4.11.1") } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1486ffe8..4e07d606 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ ? = null - var validationSid: String? = null + // TODO: 10-Oct-21 remove this var json: JSONObject? = null override fun toString(): String { diff --git a/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt b/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt index 3f9adcab..d446c464 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt @@ -7,6 +7,7 @@ import android.text.SpannableString import android.text.style.StyleSpan import androidx.core.content.ContextCompat import com.meloda.fast.R +import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkUser @@ -16,6 +17,88 @@ import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem object VkUtils { + fun attachmentToString( + attachmentClass: Class, + id: Int, + ownerId: Int, + withAccessKey: Boolean, + accessKey: String? + ): String { + val type = when (attachmentClass) { + VkAudio::class.java -> "audio" + VkFile::class.java -> "doc" + VkVideo::class.java -> "video" + VkPhoto::class.java -> "photo" + else -> throw IllegalArgumentException("unknown attachment class: $attachmentClass") + } + + val result = StringBuilder(type).append(ownerId).append('_').append(id) + if (withAccessKey && !accessKey.isNullOrBlank()) { + result.append('_') + result.append(accessKey) + } + return result.toString() + } + + + fun getMessageUser(message: VkMessage, profiles: Map): VkUser? { + return (if (!message.isUser()) null + else profiles[message.fromId]).also { message.user.value = it } + } + + fun getMessageGroup(message: VkMessage, groups: Map): VkGroup? { + return (if (!message.isGroup()) null + else groups[message.fromId]).also { message.group.value = it } + } + + fun getMessageAvatar( + message: VkMessage, + messageUser: VkUser?, + messageGroup: VkGroup? + ): String? { + return when { + message.isUser() -> messageUser?.photo200 + message.isGroup() -> messageGroup?.photo200 + else -> null + } + } + + fun getMessageTitle( + message: VkMessage, + messageUser: VkUser?, + messageGroup: VkGroup? + ): String? { + return when { + message.isUser() -> messageUser?.fullName + message.isGroup() -> messageGroup?.name + else -> null + } + } + + fun getConversationUser(conversation: VkConversation, profiles: Map): VkUser? { + return (if (!conversation.isUser()) null + else profiles[conversation.id]).also { conversation.user.value = it } + } + + fun getConversationGroup(conversation: VkConversation, groups: Map): VkGroup? { + return (if (!conversation.isGroup()) null + else groups[conversation.id]).also { conversation.group.value = it } + } + + fun getConversationAvatar( + conversation: VkConversation, + conversationUser: VkUser?, + conversationGroup: VkGroup? + ): String? { + return when { + conversation.ownerId == VKConstants.FAST_GROUP_ID -> null + conversation.isUser() -> conversationUser?.photo200 + conversation.isGroup() -> conversationGroup?.photo200 + conversation.isChat() -> conversation.photo200 + else -> null + } + } + fun prepareMessageText(text: String, forConversations: Boolean? = null): String { return text.apply { if (forConversations == true) replace("\n", "") @@ -42,6 +125,12 @@ object VkUtils { return forwards } + fun parseReplyMessage(baseReplyMessage: BaseVkMessage?): VkMessage? { + if (baseReplyMessage == null) return null + + return baseReplyMessage.asVkMessage() + } + fun parseAttachments(baseAttachments: List?): List? { if (baseAttachments.isNullOrEmpty()) return null @@ -77,9 +166,7 @@ object VkUtils { } BaseVkAttachmentItem.AttachmentType.VOICE -> { val voiceMessage = baseAttachment.voiceMessage ?: continue - attachments += VkVoiceMessage( - link = voiceMessage.link_mp3 - ) + attachments += voiceMessage.asVkVoiceMessage() } BaseVkAttachmentItem.AttachmentType.STICKER -> { val sticker = baseAttachment.sticker ?: continue @@ -87,9 +174,7 @@ object VkUtils { } BaseVkAttachmentItem.AttachmentType.GIFT -> { val gift = baseAttachment.gift ?: continue - attachments += VkGift( - link = gift.thumb_48 - ) + attachments += gift.asVkGift() } BaseVkAttachmentItem.AttachmentType.WALL -> { val wall = baseAttachment.wall ?: continue @@ -97,9 +182,7 @@ object VkUtils { } BaseVkAttachmentItem.AttachmentType.GRAFFITI -> { val graffiti = baseAttachment.graffiti ?: continue - attachments += VkGraffiti( - link = graffiti.url - ) + attachments += graffiti.asVkGraffiti() } BaseVkAttachmentItem.AttachmentType.POLL -> { val poll = baseAttachment.poll ?: continue @@ -115,9 +198,7 @@ object VkUtils { } BaseVkAttachmentItem.AttachmentType.CALL -> { val call = baseAttachment.call ?: continue - attachments += VkCall( - initiatorId = call.initiator_id - ) + attachments += call.asVkCall() } BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> { val groupCall = baseAttachment.groupCall ?: continue diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt index 42d221e6..c6673448 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt @@ -1,38 +1,50 @@ package com.meloda.fast.api.model import android.os.Parcelable +import androidx.lifecycle.MutableLiveData import androidx.room.Embedded import androidx.room.Entity +import androidx.room.Ignore import androidx.room.PrimaryKey +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Entity(tableName = "conversations") @Parcelize data class VkConversation( @PrimaryKey(autoGenerate = false) - val id: Int, - val ownerId: Int?, - val title: String?, - val photo200: String?, - val type: String, - val callInProgress: Boolean, - val isPhantom: Boolean, - val lastConversationMessageId: Int, - val inRead: Int, - val outRead: Int, - val isMarkedUnread: Boolean, - val lastMessageId: Int, - val unreadCount: Int?, - val membersCount: Int?, - val isPinned: Boolean, + var id: Int, + var ownerId: Int?, + var title: String?, + var photo200: String?, + var type: String, + var callInProgress: Boolean, + var isPhantom: Boolean, + var lastConversationMessageId: Int, + var inRead: Int, + var outRead: Int, + var isMarkedUnread: Boolean, + var lastMessageId: Int, + var unreadCount: Int?, + var membersCount: Int?, + var isPinned: Boolean, + var canChangePin: Boolean, @Embedded(prefix = "pinnedMessage_") var pinnedMessage: VkMessage? = null, @Embedded(prefix = "lastMessage_") - var lastMessage: VkMessage? = null + var lastMessage: VkMessage? = null, ) : Parcelable { + @Ignore + @IgnoredOnParcel + val user = MutableLiveData() + + @Ignore + @IgnoredOnParcel + val group = MutableLiveData() + fun isChat() = type == "chat" fun isUser() = type == "user" fun isGroup() = type == "group" diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt index b7e38ecb..83f8d229 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt @@ -1,17 +1,23 @@ package com.meloda.fast.api.model -import android.os.Parcelable +import androidx.lifecycle.MutableLiveData import androidx.room.Entity +import androidx.room.Ignore import androidx.room.PrimaryKey +import com.meloda.fast.api.UserConfig +import com.meloda.fast.api.VKConstants import com.meloda.fast.api.model.attachments.VkAttachment +import com.meloda.fast.base.adapter.SelectableItem +import com.meloda.fast.util.TimeUtils +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Entity(tableName = "messages") @Parcelize data class VkMessage( @PrimaryKey(autoGenerate = false) - val id: Int, - val text: String? = null, + var id: Int, + var text: String? = null, val isOut: Boolean, val peerId: Int, val fromId: Int, @@ -23,10 +29,22 @@ data class VkMessage( val actionConversationMessageId: Int? = null, val actionMessage: String? = null, val geoType: String? = null, - val important: Boolean = false, + var important: Boolean = false, + var forwards: List? = null, - var attachments: List? = null -) : Parcelable { + var attachments: List? = null, + +// @Embedded(prefix = "replyMessage_") + var replyMessage: VkMessage? = null +) : SelectableItem() { + + @Ignore + @IgnoredOnParcel + val user = MutableLiveData() + + @Ignore + @IgnoredOnParcel + val group = MutableLiveData() fun isPeerChat() = peerId > 2_000_000_000 @@ -43,40 +61,12 @@ data class VkMessage( return Action.parse(action) } - fun copyMessage( - id: Int = this.id, - text: String? = this.text, - isOut: Boolean = this.isOut, - peerId: Int = this.peerId, - fromId: Int = this.fromId, - date: Int = this.date, - randomId: Int = this.randomId, - action: String? = this.action, - actionMemberId: Int? = this.actionMemberId, - actionText: String? = this.actionText, - actionConversationMessageId: Int? = this.actionConversationMessageId, - actionMessage: String? = this.actionMessage, - geoType: String? = this.geoType, - important: Boolean = this.important - ) = VkMessage( - id = id, - text = text, - isOut = isOut, - peerId = peerId, - fromId = fromId, - date = date, - randomId = randomId, - action = action, - actionMemberId = actionMemberId, - actionText = actionText, - actionConversationMessageId = actionConversationMessageId, - actionMessage = actionMessage, - geoType = geoType, - important = important - ).also { - it.attachments = attachments - it.forwards = forwards - } + fun canEdit() = + fromId == UserConfig.userId && + (attachments == null || !VKConstants.restrictedToEditAttachments.contains( + attachments!![0].javaClass + )) && + (System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.ONE_DAY_IN_SECONDS) enum class Action(val value: String) { CHAT_CREATE("chat_create"), diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt index 26d3bc1a..2662b547 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt @@ -4,4 +4,8 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -open class VkAttachment : Parcelable \ No newline at end of file +open class VkAttachment : Parcelable { + + open fun asString(withAccessKey: Boolean = true) = "" + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt index cdf4e450..f2283066 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt @@ -1,17 +1,28 @@ package com.meloda.fast.api.model.attachments +import com.meloda.fast.api.VkUtils import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize data class VkAudio( val id: Int, + val ownerId: Int, val title: String, val artist: String, val url: String, - val duration: Int + val duration: Int, + val accessKey: String? ) : VkAttachment() { @IgnoredOnParcel val className: String = this::class.java.name + + override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString( + attachmentClass = this::class.java, + id = id, + ownerId = ownerId, + withAccessKey = withAccessKey, + accessKey = accessKey + ) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt index 43c54a00..c55143da 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt @@ -5,7 +5,12 @@ import kotlinx.parcelize.Parcelize @Parcelize data class VkCall( - val initiatorId: Int + val initiatorId: Int, + val receiverId: Int, + val state: String, + val time: Int, + val duration: Int, + val isVideo: Boolean ) : VkAttachment() { @IgnoredOnParcel diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt index e1e91542..19e7f7b1 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt @@ -1,17 +1,28 @@ package com.meloda.fast.api.model.attachments +import com.meloda.fast.api.VkUtils import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize data class VkFile( val id: Int, + val ownerId: Int, val title: String, val ext: String, val size: Int, - val url: String + val url: String, + val accessKey: String? ) : VkAttachment() { @IgnoredOnParcel val className: String = this::class.java.name + + override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString( + attachmentClass = this::class.java, + id = id, + ownerId = ownerId, + withAccessKey = withAccessKey, + accessKey = accessKey + ) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt index 2fea4243..6be29fca 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt @@ -5,7 +5,10 @@ import kotlinx.parcelize.Parcelize @Parcelize data class VkGift( - val link: String + val id: Int, + val thumb256: String?, + val thumb96: String?, + val thumb48: String ) : VkAttachment() { @IgnoredOnParcel diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt index a9b2ca9b..9ce65371 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt @@ -5,7 +5,12 @@ import kotlinx.parcelize.Parcelize @Parcelize data class VkGraffiti( - val link: String + val id: Int, + val ownerId: Int, + val url: String, + val width: Int, + val height: Int, + val accessKey: String ) : VkAttachment() { @IgnoredOnParcel diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt index 2a4a9cd0..321c71ac 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt @@ -9,7 +9,7 @@ data class VkLink( val title: String?, val caption: String?, val photo: VkPhoto?, - val target: String, + val target: String?, val isFavorite: Boolean ) : VkAttachment() { diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt index 2531d5a8..8a661d4b 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt @@ -1,8 +1,11 @@ package com.meloda.fast.api.model.attachments +import androidx.room.Ignore +import com.meloda.fast.api.VkUtils import com.meloda.fast.api.model.base.attachments.BaseVkPhoto import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize +import java.util.* @Parcelize data class VkPhoto( @@ -13,17 +16,70 @@ data class VkPhoto( val hasTags: Boolean, val accessKey: String?, val sizes: List, - val text: String, + val text: String?, val userId: Int? ) : VkAttachment() { + @Ignore + @IgnoredOnParcel + private val sizesChars = Stack() + + init { + sizesChars.push('s') + sizesChars.push('m') + sizesChars.push('x') + sizesChars.push('o') + sizesChars.push('p') + sizesChars.push('q') + sizesChars.push('r') + sizesChars.push('y') + sizesChars.push('z') + sizesChars.push('w') + } + @IgnoredOnParcel val className: String = this::class.java.name - fun sizeOfType(type: Char): BaseVkPhoto.Size? { + override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString( + attachmentClass = this::class.java, + id = id, + ownerId = ownerId, + withAccessKey = withAccessKey, + accessKey = accessKey + ) + + fun getMaxSize(): BaseVkPhoto.Size? { + return getSizeOrSmaller(sizesChars.peek()) + } + + fun getSizeOrNull(type: Char): BaseVkPhoto.Size? { for (size in sizes) { - if (size.type == type.toString()) + if (size.type == type.toString()) return size + } + + return null + } + + fun getSizeOrSmaller(type: Char): BaseVkPhoto.Size? { + val photoStack = sizesChars.clone() as Stack + + val sizeIndex = photoStack.search(type) + + if (sizeIndex == -1) return null + + for (i in 0 until sizeIndex) { + photoStack.pop() + } + + for (i in 0 until photoStack.size) { + val size = getSizeOrNull(photoStack.peek()) + + if (size == null) { + photoStack.pop() + continue + } else { return size + } } return null diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVideo.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVideo.kt index 33424408..b817897c 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVideo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVideo.kt @@ -1,5 +1,6 @@ package com.meloda.fast.api.model.attachments +import com.meloda.fast.api.VkUtils import com.meloda.fast.api.model.base.attachments.BaseVkVideo import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -7,8 +8,10 @@ import kotlinx.parcelize.Parcelize @Parcelize data class VkVideo( val id: Int, + val ownerId: Int, val images: List, - val firstFrames: List? + val firstFrames: List?, + val accessKey: String? ) : VkAttachment() { @IgnoredOnParcel @@ -18,4 +21,12 @@ data class VkVideo( return images.find { it.width == width } } + override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString( + attachmentClass = this::class.java, + id = id, + ownerId = ownerId, + withAccessKey = withAccessKey, + accessKey = accessKey + ) + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt index 5ead3805..6fcce196 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt @@ -5,9 +5,18 @@ import kotlinx.parcelize.Parcelize @Parcelize data class VkVoiceMessage( - val link: String + val id: Int, + val ownerId: Int, + val duration: Int, + val waveform: List, + val linkOgg: String, + val linkMp3: String, + val accessKey: String, + val transcriptState: String, + val transcript: String ) : VkAttachment() { @IgnoredOnParcel val className: String = this::class.java.name + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkConversation.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkConversation.kt index 25490fa1..8c7177f2 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkConversation.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkConversation.kt @@ -40,7 +40,8 @@ data class BaseVkConversation( unreadCount = unread_count, membersCount = chat_settings?.members_count, ownerId = chat_settings?.owner_id, - isPinned = sort_id.major_id > 0 + isPinned = sort_id.major_id > 0, + canChangePin = chat_settings?.acl?.can_change_pin == true ).apply { this.lastMessage = lastMessage this.pinnedMessage = chat_settings?.pinned_message?.asVkMessage() diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt index 8d2a57cb..f431fbff 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt @@ -23,7 +23,8 @@ data class BaseVkMessage( val payload: String, val geo: Geo?, val action: Action?, - val ttl: Int + val ttl: Int, + val reply_message: BaseVkMessage? ) : Parcelable { fun asVkMessage() = VkMessage( @@ -44,6 +45,7 @@ data class BaseVkMessage( ).also { it.attachments = VkUtils.parseAttachments(attachments) it.forwards = VkUtils.parseForwards(fwd_messages) + it.replyMessage = VkUtils.parseReplyMessage(reply_message) } @Parcelize diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt index a497bd44..09de47b9 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt @@ -13,7 +13,7 @@ data class BaseVkAudio( val url: String, val date: Int, val owner_id: Int, - val access_key: String, + val access_key: String?, val is_explicit: Boolean, val is_focus_track: Boolean, val is_licensed: Boolean, @@ -27,10 +27,12 @@ data class BaseVkAudio( fun asVkAudio() = VkAudio( id = id, + ownerId = owner_id, title = title, artist = artist, url = url, - duration = duration + duration = duration, + accessKey = access_key ) @Parcelize diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt index 9911e79a..2bbde082 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.model.base.attachments import android.os.Parcelable +import com.meloda.fast.api.model.attachments.VkCall import kotlinx.parcelize.Parcelize @Parcelize @@ -11,4 +12,15 @@ data class BaseVkCall( val time: Int, val duration: Int, val video: Boolean -) : Parcelable \ No newline at end of file +) : Parcelable { + + fun asVkCall() = VkCall( + initiatorId = initiator_id, + receiverId = receiver_id, + state = state, + time = time, + duration = duration, + isVideo = video + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt index 07b2a967..8c09507f 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt @@ -16,16 +16,18 @@ data class BaseVkFile( val url: String, val preview: Preview?, val ic_licensed: Int, - val access_key: String, + val access_key: String?, val web_preview_url: String? ) : BaseVkAttachment() { fun asVkFile() = VkFile( id = id, + ownerId = owner_id, title = title, ext = ext, url = url, - size = size + size = size, + accessKey = access_key ) @Parcelize diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt index 2ed684ff..29e646b9 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.model.base.attachments import android.os.Parcelable +import com.meloda.fast.api.model.attachments.VkGift import kotlinx.parcelize.Parcelize @Parcelize @@ -9,4 +10,13 @@ data class BaseVkGift( val thumb_256: String?, val thumb_96: String?, val thumb_48: String -) : Parcelable \ No newline at end of file +) : Parcelable { + + fun asVkGift() = VkGift( + id = id, + thumb256 = thumb_256, + thumb96 = thumb_96, + thumb48 = thumb_48 + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt index da07731a..c5e841ef 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.model.base.attachments import android.os.Parcelable +import com.meloda.fast.api.model.attachments.VkGraffiti import kotlinx.parcelize.Parcelize @Parcelize @@ -11,4 +12,15 @@ data class BaseVkGraffiti( val width: Int, val height: Int, val access_key: String -) : Parcelable \ No newline at end of file +) : Parcelable { + + fun asVkGraffiti() = VkGraffiti( + id = id, + ownerId = owner_id, + url = url, + width = width, + height = height, + accessKey = access_key + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt index c127f0c2..4f1b59c5 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt @@ -9,7 +9,7 @@ data class BaseVkLink( val title: String?, val caption: String?, val photo: BaseVkPhoto?, - val target: String, + val target: String?, val is_favorite: Boolean ) : BaseVkAttachment() { diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt index 218f7109..babe40e4 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt @@ -13,7 +13,7 @@ data class BaseVkPhoto( val has_tags: Boolean, val access_key: String?, val sizes: List, - val text: String, + val text: String?, val user_id: Int?, val lat: Double?, val long: Double?, diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt index c659c095..258e7a97 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt @@ -26,7 +26,7 @@ data class BaseVkVideo( val can_add_to_faves: Int, val can_add: Int, val can_attach_link: Int, - val access_key: String, + val access_key: String?, val owner_id: Int, val ov_id: String, val is_favorite: Boolean, @@ -40,8 +40,10 @@ data class BaseVkVideo( fun asVkVideo() = VkVideo( id = id, + ownerId = owner_id, images = image, - firstFrames = first_frame + firstFrames = first_frame, + accessKey = access_key ) @Parcelize diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt index 88b37355..4445ffe0 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.model.base.attachments import android.os.Parcelable +import com.meloda.fast.api.model.attachments.VkVoiceMessage import kotlinx.parcelize.Parcelize @Parcelize @@ -14,4 +15,18 @@ data class BaseVkVoiceMessage( val access_key: String, val transcript_state: String, val transcript: String -) : Parcelable \ No newline at end of file +) : Parcelable { + + fun asVkVoiceMessage() = VkVoiceMessage( + id = id, + ownerId = owner_id, + duration = duration, + waveform = waveform, + linkOgg = link_ogg, + linkMp3 = link_mp3, + accessKey = access_key, + transcriptState = transcript_state, + transcript = transcript + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt deleted file mode 100644 index fda2f97f..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.meloda.fast.api.model.request - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class MessagesGetHistoryRequest( - val count: Int? = null, - val offset: Int? = null, - val peerId: Int, - val extended: Boolean? = null, - val startMessageId: Int? = null, - val rev: Boolean? = null, - val fields: String? = null, -) : Parcelable { - - val map - get() = mutableMapOf( - "peer_id" to peerId.toString() - ).apply { - count?.let { this["count"] = it.toString() } - offset?.let { this["offset"] = it.toString() } - extended?.let { this["extended"] = (if (it) 1 else 0).toString() } - startMessageId?.let { this["start_message_id"] = it.toString() } - rev?.let { this["rev"] = (if (it) 1 else 0).toString() } - fields?.let { this["fields"] = it } - } - -} - -@Parcelize -data class MessagesSendRequest( - val peerId: Int, - val randomId: Int = 0, - val message: String? = null, - val lat: Int? = null, - val lon: Int? = null, - val replyTo: Int? = null, - val stickerId: Int? = null, - val disableMentions: Boolean? = null, - val dontParseLinks: Boolean? = null -) : Parcelable { - - val map - get() = mutableMapOf( - "peer_id" to peerId.toString(), - "random_id" to randomId.toString() - ).apply { - message?.let { this["message"] = it } - lat?.let { this["lat"] = it.toString() } - lon?.let { this["lon"] = it.toString() } - replyTo?.let { this["reply_to"] = it.toString() } - stickerId?.let { this["sticker_id"] = it.toString() } - disableMentions?.let { this["disable_mentions"] = (if (it) 1 else 0).toString() } - dontParseLinks?.let { this["dont_parse_links"] = (if (it) 1 else 0).toString() } - } -} - -@Parcelize -data class MessagesMarkAsImportantRequest( - val messagesIds: List, - val important: Boolean -) : Parcelable { - - val map - get() = mutableMapOf( - "message_ids" to messagesIds.joinToString { it.toString() }, - "important" to (if (important) 1 else 0).toString() - ) - -} - -@Parcelize -data class MessagesGetLongPollServerRequest( - val needPts: Boolean, - val version: Int -) : Parcelable { - - val map - get() = mutableMapOf( - "need_pts" to (if (needPts) 1 else 0).toString(), - "version" to version.toString() - ) -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/response/UsersResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/model/response/UsersResponse.kt deleted file mode 100644 index dede65e8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/response/UsersResponse.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.meloda.fast.api.model.response - diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt b/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt index 8649e89e..b6f96691 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt @@ -17,6 +17,7 @@ class AuthInterceptor : Interceptor { builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8")) } + // TODO: 9/29/2021 crash on timeout return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build()) } diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt b/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt index 1950b7b9..70c5a758 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.network import com.meloda.fast.api.VKException +import com.meloda.fast.api.base.ApiError import com.meloda.fast.api.base.ApiResponse import okhttp3.Request import okio.IOException @@ -93,7 +94,6 @@ internal class ResultCall(proxy: Call) : CallDelegate>(proxy) if (result is Answer.Error && isVkException) if (checkErrors(call, result)) return - callback.onResponse(proxy, Response.success(result)) } @@ -105,6 +105,11 @@ internal class ResultCall(proxy: Call) : CallDelegate>(proxy) } private fun checkErrors(call: Call, result: Answer.Error): Boolean { + if (result.throwable is ApiError) { + onFailure(call, result.throwable) + return true + } + val json = JSONObject(result.throwable.message ?: "{}") return if (json.has("error")) { diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt similarity index 62% rename from app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt index d0476e13..dcbcb39c 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt @@ -1,6 +1,6 @@ package com.meloda.fast.api.network -object VKUrls { +object VkUrls { const val OAUTH = "https://oauth.vk.com" const val API = "https://api.vk.com/method" @@ -12,6 +12,10 @@ object VKUrls { object Conversations { const val Get = "$API/messages.getConversations" + const val Delete = "$API/messages.deleteConversation" + const val Pin = "$API/messages.pinConversation" + const val Unpin = "$API/messages.unpinConversation" + const val ReorderPinned = "$API/messages.reorderPinnedConversations" } object Users { @@ -24,6 +28,10 @@ object VKUrls { const val MarkAsImportant = "$API/messages.markAsImportant" const val GetLongPollServer = "$API/messages.getLongPollServer" const val GetLongPollHistory = "$API/messages.getLongPollHistory" + const val Pin = "$API/messages.pin" + const val Unpin = "$API/messages.unpin" + const val Delete = "$API/messages.delete" + const val Edit = "$API/messages.edit" } diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthDataSource.kt similarity index 62% rename from app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthDataSource.kt index 0a1f63b6..49e9daaa 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthDataSource.kt @@ -1,7 +1,5 @@ -package com.meloda.fast.api.network.datasource +package com.meloda.fast.api.network.auth -import com.meloda.fast.api.network.repo.AuthRepo -import com.meloda.fast.api.model.request.RequestAuthDirect import javax.inject.Inject class AuthDataSource @Inject constructor( diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRepo.kt new file mode 100644 index 00000000..26d3c859 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRepo.kt @@ -0,0 +1,17 @@ +package com.meloda.fast.api.network.auth + +import com.meloda.fast.api.network.Answer +import com.meloda.fast.api.network.VkUrls +import retrofit2.http.GET +import retrofit2.http.Query +import retrofit2.http.QueryMap + +interface AuthRepo { + + @GET(VkUrls.Auth.DirectAuth) + suspend fun auth(@QueryMap param: Map): Answer + + @GET(VkUrls.Auth.SendSms) + suspend fun sendSms(@Query("sid") validationSid: String): Answer + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/request/AuthRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRequest.kt similarity index 97% rename from app/src/main/kotlin/com/meloda/fast/api/model/request/AuthRequest.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRequest.kt index 6868f545..2226ba1b 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/request/AuthRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRequest.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.request +package com.meloda.fast.api.network.auth import android.os.Parcelable import com.google.gson.annotations.SerializedName diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/response/AuthResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthResponse.kt similarity index 94% rename from app/src/main/kotlin/com/meloda/fast/api/model/response/AuthResponse.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthResponse.kt index 23e6c81f..2da17dee 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/response/AuthResponse.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthResponse.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.response +package com.meloda.fast.api.network.auth import android.os.Parcelable import com.google.gson.annotations.SerializedName diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsDataSource.kt new file mode 100644 index 00000000..838a5a8f --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsDataSource.kt @@ -0,0 +1,22 @@ +package com.meloda.fast.api.network.conversations + +import com.meloda.fast.api.model.VkConversation +import com.meloda.fast.database.dao.ConversationsDao +import javax.inject.Inject + +class ConversationsDataSource @Inject constructor( + private val repo: ConversationsRepo, + private val dao: ConversationsDao +) { + + suspend fun get(params: ConversationsGetRequest) = repo.get(params.map) + + suspend fun delete(params: ConversationsDeleteRequest) = repo.delete(params.map) + + suspend fun pin(params: ConversationsPinRequest) = repo.pin(params.map) + + suspend fun unpin(params: ConversationsUnpinRequest) = repo.unpin(params.map) + + suspend fun store(conversations: List) = dao.insert(conversations) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRepo.kt new file mode 100644 index 00000000..1561e74d --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRepo.kt @@ -0,0 +1,32 @@ +package com.meloda.fast.api.network.conversations + +import com.meloda.fast.api.base.ApiResponse +import com.meloda.fast.api.network.Answer +import com.meloda.fast.api.network.VkUrls +import retrofit2.http.FieldMap +import retrofit2.http.FormUrlEncoded +import retrofit2.http.POST + +interface ConversationsRepo { + + @FormUrlEncoded + @POST(VkUrls.Conversations.Get) + suspend fun get(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Conversations.Delete) + suspend fun delete(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Conversations.Pin) + suspend fun pin(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Conversations.Unpin) + suspend fun unpin(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Conversations.ReorderPinned) + suspend fun reorderPinned(@FieldMap params: Map): Answer> + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/request/ConversationsRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRequest.kt similarity index 60% rename from app/src/main/kotlin/com/meloda/fast/api/model/request/ConversationsRequest.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRequest.kt index d5a1e66e..9bd9a622 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/request/ConversationsRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRequest.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.request +package com.meloda.fast.api.network.conversations import android.os.Parcelable import kotlinx.parcelize.Parcelize @@ -23,4 +23,19 @@ data class ConversationsGetRequest( extended?.let { this["extended"] = it.toString() } startMessageId?.let { this["start_message_id"] = it.toString() } } +} + +@Parcelize +data class ConversationsDeleteRequest(val peerId: Int) : Parcelable { + val map get() = mapOf("peer_id" to peerId.toString()) +} + +@Parcelize +data class ConversationsPinRequest(val peerId: Int) : Parcelable { + val map get() = mapOf("peer_id" to peerId.toString()) +} + +@Parcelize +data class ConversationsUnpinRequest(val peerId: Int) : Parcelable { + val map get() = mapOf("peer_id" to peerId.toString()) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/response/ConversationsResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsResponse.kt similarity index 93% rename from app/src/main/kotlin/com/meloda/fast/api/model/response/ConversationsResponse.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsResponse.kt index e3e1a821..c49d24e1 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/response/ConversationsResponse.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsResponse.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.response +package com.meloda.fast.api.network.conversations import android.os.Parcelable import com.google.gson.annotations.SerializedName diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/ConversationsDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/ConversationsDataSource.kt deleted file mode 100644 index 198bf9b3..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/ConversationsDataSource.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.meloda.fast.api.network.datasource - -import com.meloda.fast.api.model.VkConversation -import com.meloda.fast.api.network.repo.ConversationsRepo -import com.meloda.fast.api.model.request.ConversationsGetRequest -import com.meloda.fast.database.dao.ConversationsDao -import javax.inject.Inject - -class ConversationsDataSource @Inject constructor( - private val repo: ConversationsRepo, - private val dao: ConversationsDao -) { - - suspend fun getAllChats(params: ConversationsGetRequest) = repo.getAllChats(params.map) - - suspend fun storeConversations(conversations: List) = dao.insert(conversations) - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/LongPollRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRepo.kt similarity index 90% rename from app/src/main/kotlin/com/meloda/fast/api/network/repo/LongPollRepo.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRepo.kt index 793f0940..997626d8 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/LongPollRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRepo.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.network.repo +package com.meloda.fast.api.network.longpoll import com.meloda.fast.api.base.ApiResponse import com.meloda.fast.api.network.Answer diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt similarity index 55% rename from app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt index 5995d515..1fee173b 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt @@ -1,11 +1,6 @@ -package com.meloda.fast.api.network.datasource +package com.meloda.fast.api.network.messages import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.request.MessagesGetHistoryRequest -import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest -import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest -import com.meloda.fast.api.model.request.MessagesSendRequest -import com.meloda.fast.api.network.repo.MessagesRepo import com.meloda.fast.database.dao.MessagesDao import javax.inject.Inject @@ -26,8 +21,20 @@ class MessagesDataSource @Inject constructor( suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) = repo.getLongPollServer(params.map) - suspend fun storeMessages(messages: List) = dao.insert(messages) + suspend fun pin(params: MessagesPinMessageRequest) = + repo.pin(params.map) - suspend fun getCachedMessages(peerId: Int) = dao.getByPeerId(peerId) + suspend fun unpin(params: MessagesUnPinMessageRequest) = + repo.unpin(params.map) + + suspend fun delete(params: MessagesDeleteRequest) = + repo.delete(params.map) + + suspend fun edit(params: MessagesEditRequest) = + repo.edit(params.map) + + suspend fun store(messages: List) = dao.insert(messages) + + suspend fun getCached(peerId: Int) = dao.getByPeerId(peerId) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt new file mode 100644 index 00000000..729414c9 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt @@ -0,0 +1,46 @@ +package com.meloda.fast.api.network.messages + +import com.meloda.fast.api.base.ApiResponse +import com.meloda.fast.api.model.base.BaseVkLongPoll +import com.meloda.fast.api.model.base.BaseVkMessage +import com.meloda.fast.api.network.Answer +import com.meloda.fast.api.network.VkUrls +import retrofit2.http.FieldMap +import retrofit2.http.FormUrlEncoded +import retrofit2.http.POST + +interface MessagesRepo { + + @FormUrlEncoded + @POST(VkUrls.Messages.GetHistory) + suspend fun getHistory(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Messages.Send) + suspend fun send(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Messages.MarkAsImportant) + suspend fun markAsImportant(@FieldMap params: Map): Answer>> + + @FormUrlEncoded + @POST(VkUrls.Messages.GetLongPollServer) + suspend fun getLongPollServer(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Messages.Pin) + suspend fun pin(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Messages.Unpin) + suspend fun unpin(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Messages.Delete) + suspend fun delete(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Messages.Edit) + suspend fun edit(@FieldMap params: Map): Answer> + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt new file mode 100644 index 00000000..a96918cd --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt @@ -0,0 +1,168 @@ +package com.meloda.fast.api.network.messages + +import android.os.Parcelable +import com.meloda.fast.api.ApiExtensions.intString +import com.meloda.fast.api.model.attachments.VkAttachment +import kotlinx.parcelize.Parcelize + +@Parcelize +data class MessagesGetHistoryRequest( + val count: Int? = null, + val offset: Int? = null, + val peerId: Int, + val extended: Boolean? = null, + val startMessageId: Int? = null, + val rev: Boolean? = null, + val fields: String? = null, +) : Parcelable { + + val map + get() = mutableMapOf( + "peer_id" to peerId.toString() + ).apply { + count?.let { this["count"] = it.toString() } + offset?.let { this["offset"] = it.toString() } + extended?.let { this["extended"] = it.intString } + startMessageId?.let { this["start_message_id"] = it.toString() } + rev?.let { this["rev"] = it.intString } + fields?.let { this["fields"] = it } + } + +} + +@Parcelize +data class MessagesSendRequest( + val peerId: Int, + val randomId: Int = 0, + val message: String? = null, + val lat: Int? = null, + val lon: Int? = null, + val replyTo: Int? = null, + val stickerId: Int? = null, + val disableMentions: Boolean? = null, + val dontParseLinks: Boolean? = null +) : Parcelable { + + val map + get() = mutableMapOf( + "peer_id" to peerId.toString(), + "random_id" to randomId.toString() + ).apply { + message?.let { this["message"] = it } + lat?.let { this["lat"] = it.toString() } + lon?.let { this["lon"] = it.toString() } + replyTo?.let { this["reply_to"] = it.toString() } + stickerId?.let { this["sticker_id"] = it.toString() } + disableMentions?.let { this["disable_mentions"] = it.intString } + dontParseLinks?.let { this["dont_parse_links"] = it.intString } + } +} + +@Parcelize +data class MessagesMarkAsImportantRequest( + val messagesIds: List, + val important: Boolean +) : Parcelable { + + val map + get() = mutableMapOf( + "message_ids" to messagesIds.joinToString { it.toString() }, + "important" to important.intString + ) + +} + +@Parcelize +data class MessagesGetLongPollServerRequest( + val needPts: Boolean, + val version: Int +) : Parcelable { + + val map + get() = mutableMapOf( + "need_pts" to needPts.intString, + "version" to version.toString() + ) +} + + +@Parcelize +data class MessagesPinMessageRequest( + val peerId: Int, + val messageId: Int? = null, + val conversationMessageId: Int? = null +) : Parcelable { + + val map + get() = mutableMapOf( + "peer_id" to peerId.toString() + ).apply { + messageId?.let { this["message_id"] = it.toString() } + conversationMessageId?.let { this["conversation_message_id"] = it.toString() } + } + +} + +@Parcelize +data class MessagesUnPinMessageRequest(val peerId: Int) : Parcelable { + val map get() = mutableMapOf("peer_id" to peerId.toString()) +} + +@Parcelize +data class MessagesDeleteRequest( + val peerId: Int, + val messagesIds: List? = null, + val conversationsMessagesIds: List? = null, + val isSpam: Boolean? = null, + val deleteForAll: Boolean? = null +) : Parcelable { + + val map + get() = mutableMapOf( + "peer_id" to peerId.toString() + ).apply { + isSpam?.let { this["spam"] = it.intString } + deleteForAll?.let { this["delete_for_all"] = it.intString } + messagesIds?.let { + this["message_ids"] = it.joinToString { id -> id.toString() } + } + + conversationsMessagesIds?.let { + this["conversation_message_ids"] = it.joinToString { id -> id.toString() } + } + } +} + +@Parcelize +data class MessagesEditRequest( + val peerId: Int, + val messageId: Int, + val message: String? = null, + val lat: Float? = null, + val lon: Float? = null, + val attachments: List? = null, + val notParseLinks: Boolean = false, + val keepSnippets: Boolean = true, + val keepForwardedMessages: Boolean = true +) : Parcelable { + + val map + get() = mutableMapOf( + "peer_id" to peerId.toString(), + "message_id" to messageId.toString(), + "dont_parse_links" to notParseLinks.intString, + "keep_snippets" to keepSnippets.intString, + "keep_forward_messages" to keepForwardedMessages.intString + ).apply { + message?.let { this["message"] = it } + lat?.let { this["lat"] = it.toString() } + lon?.let { this["lon"] = it.toString() } + attachments?.let { + val attachments = + if (it.isEmpty()) "" + else it.joinToString(separator = ",") { attachment -> attachment.asString() } + this["attachment"] = attachments + } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/response/MessagesResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesResponse.kt similarity index 92% rename from app/src/main/kotlin/com/meloda/fast/api/model/response/MessagesResponse.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesResponse.kt index 9ddeeb24..1462c031 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/response/MessagesResponse.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesResponse.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.response +package com.meloda.fast.api.network.messages import android.os.Parcelable import com.meloda.fast.api.model.base.BaseVkConversation diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt deleted file mode 100644 index 60fed0f8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.meloda.fast.api.network.repo - -import com.meloda.fast.api.network.VKUrls -import com.meloda.fast.api.model.response.ResponseAuthDirect -import com.meloda.fast.api.network.Answer -import com.meloda.fast.api.model.response.ResponseSendSms -import retrofit2.http.* - -interface AuthRepo { - - @GET(VKUrls.Auth.DirectAuth) - suspend fun auth(@QueryMap param: Map): Answer - - @GET(VKUrls.Auth.SendSms) - suspend fun sendSms(@Query("sid") validationSid: String): Answer - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt deleted file mode 100644 index 9f0c8d4c..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.meloda.fast.api.network.repo - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.network.Answer -import com.meloda.fast.api.network.VKUrls -import com.meloda.fast.api.model.response.ConversationsGetResponse -import retrofit2.http.FieldMap -import retrofit2.http.FormUrlEncoded -import retrofit2.http.POST - -interface ConversationsRepo { - - @FormUrlEncoded - @POST(VKUrls.Conversations.Get) - suspend fun getAllChats(@FieldMap params: Map): Answer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt deleted file mode 100644 index 831d4c49..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.meloda.fast.api.network.repo - -import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.model.base.BaseVkLongPoll -import com.meloda.fast.api.model.response.MessagesGetHistoryResponse -import com.meloda.fast.api.network.Answer -import com.meloda.fast.api.network.VKUrls -import retrofit2.http.FieldMap -import retrofit2.http.FormUrlEncoded -import retrofit2.http.POST - -interface MessagesRepo { - - @FormUrlEncoded - @POST(VKUrls.Messages.GetHistory) - suspend fun getHistory(@FieldMap params: Map): Answer> - - @FormUrlEncoded - @POST(VKUrls.Messages.Send) - suspend fun send(@FieldMap params: Map): Answer> - - @FormUrlEncoded - @POST(VKUrls.Messages.MarkAsImportant) - suspend fun markAsImportant(@FieldMap params: Map): Answer>> - - @FormUrlEncoded - @POST(VKUrls.Messages.GetLongPollServer) - suspend fun getLongPollServer(@FieldMap params: Map): Answer> - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/UsersDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersDataSource.kt similarity index 70% rename from app/src/main/kotlin/com/meloda/fast/api/network/datasource/UsersDataSource.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/users/UsersDataSource.kt index 7e9939cf..86a7b88a 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/UsersDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersDataSource.kt @@ -1,8 +1,6 @@ -package com.meloda.fast.api.network.datasource +package com.meloda.fast.api.network.users import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.network.repo.UsersRepo -import com.meloda.fast.api.model.request.UsersGetRequest import com.meloda.fast.database.dao.UsersDao import javax.inject.Inject diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/UsersRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersRepo.kt similarity index 77% rename from app/src/main/kotlin/com/meloda/fast/api/network/repo/UsersRepo.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/users/UsersRepo.kt index 782cb32f..7db8e122 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/UsersRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersRepo.kt @@ -1,9 +1,9 @@ -package com.meloda.fast.api.network.repo +package com.meloda.fast.api.network.users import com.meloda.fast.api.base.ApiResponse import com.meloda.fast.api.model.base.BaseVkUser import com.meloda.fast.api.network.Answer -import com.meloda.fast.api.network.VKUrls +import com.meloda.fast.api.network.VkUrls import retrofit2.http.FieldMap import retrofit2.http.FormUrlEncoded import retrofit2.http.POST @@ -11,7 +11,7 @@ import retrofit2.http.POST interface UsersRepo { @FormUrlEncoded - @POST(VKUrls.Users.GetById) + @POST(VkUrls.Users.GetById) suspend fun getById( @FieldMap params: Map? ): Answer>> diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/request/UsersRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersRequest.kt similarity index 92% rename from app/src/main/kotlin/com/meloda/fast/api/model/request/UsersRequest.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/users/UsersRequest.kt index afb9b2ab..5a88dec5 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/request/UsersRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersRequest.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.request +package com.meloda.fast.api.network.users import android.os.Parcelable import kotlinx.parcelize.Parcelize diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt new file mode 100644 index 00000000..c4084e58 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt @@ -0,0 +1,2 @@ +package com.meloda.fast.api.network.users + diff --git a/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt b/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt index 1c910dad..a3e6804e 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt @@ -1,13 +1,11 @@ package com.meloda.fast.base import android.os.Bundle -import android.view.View import androidx.annotation.LayoutRes import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry -import com.google.android.material.snackbar.Snackbar abstract class BaseActivity : AppCompatActivity, LifecycleOwner { @@ -39,10 +37,4 @@ abstract class BaseActivity : AppCompatActivity, LifecycleOwner { lifecycleRegistry.currentState = Lifecycle.State.DESTROYED } - val rootView: View? get() = findViewById(android.R.id.content) - - fun requireRootView() = rootView!! - - var errorSnackbar: Snackbar? = null - } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/BaseViewModelFragment.kt b/app/src/main/kotlin/com/meloda/fast/base/BaseViewModelFragment.kt index cb21a3e1..40ddc1f0 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/BaseViewModelFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/BaseViewModelFragment.kt @@ -11,7 +11,7 @@ import com.meloda.fast.activity.MainActivity import com.meloda.fast.api.UserConfig import com.meloda.fast.base.viewmodel.BaseViewModel import com.meloda.fast.base.viewmodel.IllegalTokenEvent -import com.meloda.fast.base.viewmodel.VKEvent +import com.meloda.fast.base.viewmodel.VkEvent import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach @@ -30,7 +30,7 @@ abstract class BaseViewModelFragment : BaseFragment { } } - protected open fun onEvent(event: VKEvent) { + protected open fun onEvent(event: VkEvent) { if (event is IllegalTokenEvent) { Toast.makeText( requireContext(), R.string.authorization_failed, Toast.LENGTH_LONG diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt index e77ba993..74870bc6 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt @@ -114,7 +114,7 @@ abstract class BaseAdapter( holder.bind(position) } - protected fun initListeners(itemView: View, position: Int) { + protected open fun initListeners(itemView: View, position: Int) { if (itemView is AdapterView<*>) return itemView.setOnClickListener { itemClickListener.invoke(position) } diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/Items.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/Items.kt new file mode 100644 index 00000000..cfa64546 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/base/adapter/Items.kt @@ -0,0 +1,15 @@ +package com.meloda.fast.base.adapter + +import android.os.Parcelable +import androidx.room.Ignore +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +open class SelectableItem : Parcelable { + + @Ignore + @IgnoredOnParcel + var isSelected: Boolean = false + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt index 4892ca01..8ce65c7e 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt @@ -15,7 +15,7 @@ abstract class BaseViewModel : ViewModel() { var unknownErrorDefaultText: String = "" - protected val tasksEventChannel = Channel() + protected val tasksEventChannel = Channel() val tasksEvent = tasksEventChannel.receiveAsFlow() protected fun makeJob( @@ -25,23 +25,35 @@ abstract class BaseViewModel : ViewModel() { onEnd: (suspend () -> Unit)? = null, onError: (suspend (Throwable) -> Unit)? = null ) = viewModelScope.launch { - onStart?.invoke() + onStart?.invoke() ?: onStart() when (val response = job()) { is Answer.Success -> onAnswer(response.data) is Answer.Error -> { checkErrors(response.throwable) - onError?.invoke(response.throwable) - ?: sendEvent( - ErrorEvent( - response.throwable.message - ?: unknownErrorDefaultText - ) - ) + onError?.invoke(response.throwable) ?: onError(response.throwable) } } - }.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } } + }.also { + it.invokeOnCompletion { + viewModelScope.launch { + onEnd?.invoke() ?: onStop() + } + } + } - protected suspend fun sendEvent(event: T) = tasksEventChannel.send(event) + protected suspend fun onStart() { + sendEvent(StartProgressEvent) + } + + protected suspend fun onStop() { + sendEvent(StopProgressEvent) + } + + protected suspend fun onError(throwable: Throwable) { + sendEvent(ErrorEvent(throwable.message ?: unknownErrorDefaultText)) + } + + protected suspend fun sendEvent(event: T) = tasksEventChannel.send(event) private suspend fun checkErrors(throwable: Throwable) { when (throwable) { diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt index 0bac43e5..52f55dfa 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt @@ -5,15 +5,15 @@ data class ShowDialogInfoEvent( val message: String, val positiveBtn: String? = null, val negativeBtn: String? = null -) : VKEvent() +) : VkEvent() -data class ErrorEvent(val errorText: String) : VKEvent() +data class ErrorEvent(val errorText: String) : VkEvent() -object IllegalTokenEvent : VKEvent() -data class CaptchaEvent(val sid: String, val image: String) : VKEvent() -data class ValidationEvent(val sid: String) : VKEvent() +object IllegalTokenEvent : VkEvent() +data class CaptchaEvent(val sid: String, val image: String) : VkEvent() +data class ValidationEvent(val sid: String) : VkEvent() -object StartProgressEvent : VKEvent() -object StopProgressEvent : VKEvent() +object StartProgressEvent : VkEvent() +object StopProgressEvent : VkEvent() -abstract class VKEvent \ No newline at end of file +abstract class VkEvent \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/common/AppSettings.kt b/app/src/main/kotlin/com/meloda/fast/common/AppSettings.kt index 1f9d8b6b..c4039eb1 100644 --- a/app/src/main/kotlin/com/meloda/fast/common/AppSettings.kt +++ b/app/src/main/kotlin/com/meloda/fast/common/AppSettings.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.Job object AppSettings { val keyIsMultilineEnabled = booleanPreferencesKey("isMultilineEnabled") + } val Context.dataStore: DataStore by preferencesDataStore( diff --git a/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt b/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt index eb0a7ea4..e8d5c859 100644 --- a/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt +++ b/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt @@ -19,7 +19,7 @@ import com.meloda.fast.database.dao.UsersDao VkUser::class, VkGroup::class ], - version = 24, + version = 26, exportSchema = false, ) @TypeConverters(Converters::class) diff --git a/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt b/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt index 3ebaa503..989d3402 100644 --- a/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt +++ b/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt @@ -4,11 +4,15 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import com.meloda.fast.api.network.AuthInterceptor import com.meloda.fast.api.network.ResultCallFactory -import com.meloda.fast.api.network.datasource.AuthDataSource -import com.meloda.fast.api.network.datasource.ConversationsDataSource -import com.meloda.fast.api.network.datasource.MessagesDataSource -import com.meloda.fast.api.network.datasource.UsersDataSource -import com.meloda.fast.api.network.repo.* +import com.meloda.fast.api.network.auth.AuthRepo +import com.meloda.fast.api.network.auth.AuthDataSource +import com.meloda.fast.api.network.conversations.ConversationsDataSource +import com.meloda.fast.api.network.conversations.ConversationsRepo +import com.meloda.fast.api.network.longpoll.LongPollRepo +import com.meloda.fast.api.network.messages.MessagesDataSource +import com.meloda.fast.api.network.users.UsersDataSource +import com.meloda.fast.api.network.messages.MessagesRepo +import com.meloda.fast.api.network.users.UsersRepo import com.meloda.fast.database.dao.ConversationsDao import com.meloda.fast.database.dao.MessagesDao import com.meloda.fast.database.dao.UsersDao diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/ContextExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/ContextExtensions.kt deleted file mode 100644 index dcce0bf6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/ContextExtensions.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.meloda.fast.extensions - -import android.content.Context -import android.graphics.Typeface -import android.graphics.drawable.Drawable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.* -import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat - -object ContextExtensions { - - fun Context.drawable(@DrawableRes resId: Int): Drawable? { - return ContextCompat.getDrawable(this, resId) - } - - @ColorInt - fun Context.color(@ColorRes resId: Int): Int { - return ContextCompat.getColor(this, resId) - } - - fun Context.font(@FontRes resId: Int): Typeface? { - return ResourcesCompat.getFont(this, resId) - } - - fun Context.string(@StringRes resId: Int): String { - return getString(resId) - } - - fun Context.view(resId: Int, root: ViewGroup? = null, attachToRoot: Boolean = false): View { - return LayoutInflater.from(this).inflate(resId, root, attachToRoot) - } - - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/DrawableExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/DrawableExtensions.kt deleted file mode 100644 index 579e0442..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/DrawableExtensions.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.extensions - -import android.graphics.drawable.Drawable -import androidx.annotation.ColorInt - -object DrawableExtensions { - - fun Drawable?.tint(@ColorInt color: Int): Drawable? { - this?.setTint(color) - return this - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/Extensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/Extensions.kt deleted file mode 100644 index 9c308106..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/Extensions.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.meloda.fast.extensions - -import android.graphics.* -import kotlin.math.min - -fun Bitmap.borderedCircularBitmap( - borderColor: Int = 0, - borderWidth: Int = 0 -): Bitmap? { - val bitmap = Bitmap.createBitmap( - width, // width in pixels - height, // height in pixels - Bitmap.Config.ARGB_8888 - ) - - // canvas to draw circular bitmap - val canvas = Canvas(bitmap) - - // get the maximum radius - val radius = min(width / 2f, height / 2f) - - // create a path to draw circular bitmap border - val borderPath = Path().apply { - addCircle( - width / 2f, - height / 2f, - radius, - Path.Direction.CCW - ) - } - - // draw border on circular bitmap - canvas.clipPath(borderPath) - canvas.drawColor(borderColor) - - - // create a path for circular bitmap - val bitmapPath = Path().apply { - addCircle( - width / 2f, - height / 2f, - radius - borderWidth, - Path.Direction.CCW - ) - } - - canvas.clipPath(bitmapPath) - val paint = Paint().apply { - xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) - isAntiAlias = true - } - - // clear the circular bitmap drawing area - // it will keep bitmap transparency - canvas.drawBitmap(this, 0f, 0f, paint) - - // now draw the circular bitmap - canvas.drawBitmap(this, 0f, 0f, null) - - - val diameter = (radius * 2).toInt() - val x = (width - diameter) / 2 - val y = (height - diameter) / 2 - - // return cropped circular bitmap with border - return Bitmap.createBitmap( - bitmap, // source bitmap - x, // x coordinate of the first pixel in source - y, // y coordinate of the first pixel in source - diameter, // width - diameter // height - ) -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/FloatExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/FloatExtensions.kt deleted file mode 100644 index 42fdca5e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/FloatExtensions.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.extensions - -import kotlin.math.roundToInt - -object FloatExtensions { - - fun Float.int(): Int { - return roundToInt() - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/LiveDataExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/LiveDataExtensions.kt deleted file mode 100644 index 552855be..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/LiveDataExtensions.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.meloda.fast.extensions - -import androidx.annotation.UiThread -import androidx.lifecycle.MutableLiveData - -object LiveDataExtensions { - - operator fun MutableLiveData>.set(position: Int, v: T) { - val value = (this.value ?: arrayListOf()).apply { this[position] = v } - this.value = value - } - - operator fun MutableLiveData>.get(position: Int): T { - return (value as MutableList)[position] - } - - @JvmOverloads - fun MutableLiveData>.add(v: T, position: Int = -1) { - val value = (this.value ?: arrayListOf()).apply { - if (position == -1) this.add(v) else this.add(position, v) - } - - this.value = value - } - - @JvmOverloads - fun MutableLiveData>.addAll(values: List, position: Int = -1) { - val value = (this.value ?: arrayListOf()).apply { - if (position == -1) this.addAll(values) - else this.addAll(position, values) - } - - this.value = value - } - - @Suppress("TYPE_INFERENCE_ONLY_INPUT_TYPES_WARNING") - fun MutableLiveData>.removeAll(values: List) { - val value = (this.value ?: arrayListOf()).apply { - this.removeAll(values) - } - - this.value = value - } - - fun MutableLiveData>.removeAt(index: Int) { - val value = (this.value ?: arrayListOf()).apply { - this.removeAt(index) - } - - this.value = value - } - - fun MutableLiveData>.remove(item: T) { - val value = (this.value ?: arrayListOf()).apply { - this.remove(item) - } - - this.value = value - } - - operator fun MutableLiveData>.iterator(): Iterator { - return (value as MutableList).iterator() - } - - fun MutableLiveData>.clear() { - value = arrayListOf() - } - - val MutableLiveData>.indices get() = (value as MutableList).indices - - val MutableLiveData>.size get() = (value as MutableList).size - - fun MutableLiveData>.isEmpty(): Boolean { - return (value as MutableList).isEmpty() - } - - fun MutableLiveData>.isNotEmpty(): Boolean { - return !isEmpty() - } - - fun MutableLiveData>.requireValue() = value!! - - @UiThread - operator fun MutableLiveData>.plusAssign(values: List) { - val value = (this.value ?: arrayListOf()).apply { - this.addAll(values) - } - - this.value = value - } - - operator fun MutableLiveData>.plusAssign(v: T) { - val value = (this.value ?: arrayListOf()).apply { - this.add(v) - } - - this.value = value - } - - operator fun MutableLiveData>.minusAssign(values: List) { - val value = (this.value ?: arrayListOf()).apply { - this.removeAll(values) - } - - this.value = value - } - - operator fun MutableLiveData>.minusAssign(v: T) { - val value = (this.value ?: arrayListOf()).apply { - this.remove(v) - } - - this.value = value - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/StringExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/StringExtensions.kt deleted file mode 100644 index 7d2bde6a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/StringExtensions.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.extensions - -import java.util.* - -object StringExtensions { - - fun String.lowerCase(): String { - return toLowerCase(Locale.getDefault()) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/TextViewExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/TextViewExtensions.kt index 9d130722..b4a7ce18 100644 --- a/app/src/main/kotlin/com/meloda/fast/extensions/TextViewExtensions.kt +++ b/app/src/main/kotlin/com/meloda/fast/extensions/TextViewExtensions.kt @@ -1,17 +1,11 @@ package com.meloda.fast.extensions import android.widget.TextView -import com.google.android.material.textfield.TextInputLayout object TextViewExtensions { fun TextView.clear() { - text = "" + text = null } - fun TextInputLayout.clear() { - editText?.setText("") - } - - } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/io/BytesOutputStream.kt b/app/src/main/kotlin/com/meloda/fast/io/BytesOutputStream.kt deleted file mode 100644 index 65892969..00000000 --- a/app/src/main/kotlin/com/meloda/fast/io/BytesOutputStream.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.meloda.fast.io - -import java.io.ByteArrayOutputStream - -class BytesOutputStream : ByteArrayOutputStream { - constructor() : super(8192) - constructor(size: Int) : super(size) - - val byteArray: ByteArray = buf -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/io/Charsets.kt b/app/src/main/kotlin/com/meloda/fast/io/Charsets.kt deleted file mode 100644 index d9de4363..00000000 --- a/app/src/main/kotlin/com/meloda/fast/io/Charsets.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.meloda.fast.io - -import java.nio.charset.Charset -import java.nio.charset.StandardCharsets - -object Charsets { - - val ASCII: Charset = StandardCharsets.US_ASCII - - val UTF_8: Charset = StandardCharsets.UTF_8 - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/io/EasyStreams.kt b/app/src/main/kotlin/com/meloda/fast/io/EasyStreams.kt deleted file mode 100644 index af371aa5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/io/EasyStreams.kt +++ /dev/null @@ -1,174 +0,0 @@ -package com.meloda.fast.io - -import org.jetbrains.annotations.Contract -import java.io.* -import java.nio.charset.Charset -import java.util.zip.GZIPInputStream -import java.util.zip.GZIPOutputStream -import kotlin.math.max - -object EasyStreams { - - const val BUFFER_SIZE = 8192 - const val CHAR_BUFFER_SIZE = 4096 - - @JvmOverloads - @Throws(IOException::class) - fun read(from: InputStream, encoding: Charset? = Charsets.UTF_8): String { - return read(InputStreamReader(from, encoding)) - } - - @JvmStatic - @Throws(IOException::class) - fun read(from: Reader): String { - val builder = StringWriter(CHAR_BUFFER_SIZE) - return try { - copy(from, builder) - builder.toString() - } finally { - close(from) - } - } - - @JvmStatic - @Throws(IOException::class) - fun readBytes(from: InputStream): ByteArray { - val output = ByteArrayOutputStream(max(from.available(), BUFFER_SIZE)) - try { - copy(from, output) - } finally { - close(from) - } - return output.toByteArray() - } - - @Throws(IOException::class) - fun write(from: ByteArray?, to: OutputStream) { - try { - to.write(from) - to.flush() - } finally { - close(to) - } - } - - @Throws(IOException::class) - fun write(from: String?, to: OutputStream?) { - write(from, OutputStreamWriter(to, Charsets.UTF_8)) - } - - @Throws(IOException::class) - fun write(from: CharArray?, to: Writer) { - try { - to.write(from) - to.flush() - } finally { - close(to) - } - } - - @JvmStatic - @Throws(IOException::class) - fun write(from: String?, to: Writer) { - try { - to.write(from) - to.flush() - } finally { - close(to) - } - } - - @Throws(IOException::class) - fun copy(from: Reader, to: Writer): Long { - val buffer = CharArray(CHAR_BUFFER_SIZE) - var read: Int - var total: Long = 0 - while (from.read(buffer).also { read = it } != -1) { - to.write(buffer, 0, read) - total += read.toLong() - } - return total - } - - @Throws(IOException::class) - fun copy(from: InputStream, to: OutputStream): Long { - val buffer = ByteArray(BUFFER_SIZE) - var read: Int - var total: Long = 0 - while (from.read(buffer).also { read = it } != -1) { - to.write(buffer, 0, read) - total += read.toLong() - } - return total - } - - fun buffer(input: InputStream?): BufferedInputStream { - return buffer(input, BUFFER_SIZE) - } - - @Contract("null, _ -> new") - fun buffer(input: InputStream?, size: Int): BufferedInputStream { - return if (input is BufferedInputStream) input else BufferedInputStream(input, size) - } - - fun buffer(output: OutputStream?): BufferedOutputStream { - return buffer(output, BUFFER_SIZE) - } - - @Contract("null, _ -> new") - fun buffer(output: OutputStream?, size: Int): BufferedOutputStream { - return if (output is BufferedOutputStream) output else BufferedOutputStream(output, size) - } - - fun buffer(input: Reader?): BufferedReader { - return buffer(input, CHAR_BUFFER_SIZE) - } - - @Contract("null, _ -> new") - fun buffer(input: Reader?, size: Int): BufferedReader { - return if (input is BufferedReader) input else BufferedReader(input, size) - } - - fun buffer(output: Writer?): BufferedWriter { - return buffer(output, CHAR_BUFFER_SIZE) - } - - @Contract("null, _ -> new") - fun buffer(output: Writer?, size: Int): BufferedWriter { - return if (output is BufferedWriter) output else BufferedWriter(output, size) - } - - @Throws(IOException::class) - fun gzip(input: InputStream?): GZIPInputStream { - return gzip(input, BUFFER_SIZE) - } - - @Contract("null, _ -> new") - @Throws(IOException::class) - fun gzip(input: InputStream?, size: Int): GZIPInputStream { - return if (input is GZIPInputStream) input else GZIPInputStream(input, size) - } - - @Throws(IOException::class) - fun gzip(input: OutputStream?): GZIPOutputStream { - return gzip(input, BUFFER_SIZE) - } - - @Contract("null, _ -> new") - @Throws(IOException::class) - fun gzip(input: OutputStream?, size: Int): GZIPOutputStream { - return if (input is GZIPOutputStream) input else GZIPOutputStream(input, size) - } - - fun close(c: Closeable?): Boolean { - if (c != null) { - try { - c.close() - return true - } catch (e: IOException) { - e.printStackTrace() - } - } - return false - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/io/FileStreams.kt b/app/src/main/kotlin/com/meloda/fast/io/FileStreams.kt deleted file mode 100644 index f02f7e7d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/io/FileStreams.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.meloda.fast.io - -import org.jetbrains.annotations.Contract -import java.io.* -import java.math.BigInteger - -object FileStreams { - - val lineSeparatorChar = lineSeparator()[0] - - const val ONE_KB = 1024 - const val ONE_MB = ONE_KB * 1024 - const val ONE_GB = ONE_MB * 1024 - const val ONE_TB = ONE_GB * 1024L - const val ONE_PB = ONE_TB * 1024L - const val ONE_EB = ONE_PB * 1024L - - val ONE_ZB: BigInteger = BigInteger.valueOf(ONE_EB).multiply(BigInteger.valueOf(1024L)) - val ONE_YB: BigInteger = ONE_ZB.multiply(BigInteger.valueOf(1024L)) - - @Throws(IOException::class) - fun read(from: File?): String { - return EasyStreams.read(reader(from)) - } - - @Throws(IOException::class) - fun write(from: String?, to: File?) { - EasyStreams.write(from, writer(to)) - } - - @Throws(IOException::class) - fun write(from: ByteArray?, to: File?) { - EasyStreams.write(from, FileOutputStream(to)) - } - - @Throws(IOException::class) - fun append(from: ByteArray?, to: File?) { - EasyStreams.write(from, FileOutputStream(to, true)) - } - - @Throws(IOException::class) - fun append(from: CharArray?, to: File?) { - EasyStreams.write(from, FileWriter(to, true)) - } - - @Throws(IOException::class) - fun append(from: CharSequence, to: File?) { - EasyStreams.write(if (from is String) from else from.toString(), FileWriter(to, true)) - } - - fun delete(dir: File) { - if (dir.isDirectory) { - val files = dir.listFiles() ?: return - for (file in files) { - delete(file) - } - } else { - dir.delete() - } - } - - fun lineSeparator(): String { - return System.lineSeparator() - } - - fun search(dir: File, name: String?): File? { - require(dir.isDirectory) { "dir can't be file." } - - val files = dir.listFiles() ?: return null - - if (files.isEmpty()) { - return null - } - - for (file in files) { - if (file.isDirectory) { - search(file, name) - } else if (file.name.contains(name!!)) { - return file - } - } - return null - } - - @Contract("_ -> new") - @Throws(FileNotFoundException::class) - fun reader(from: File?): Reader { - return InputStreamReader(FileInputStream(from), Charsets.UTF_8) - } - - @Contract("_ -> new") - @Throws(FileNotFoundException::class) - fun writer(to: File?): Writer { - return OutputStreamWriter(FileOutputStream(to), Charsets.UTF_8) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsAdapter.kt index 2f829598..ff80af05 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsAdapter.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsAdapter.kt @@ -75,29 +75,17 @@ class ConversationsAdapter constructor( return } - val chatUser: VkUser? = if (conversation.isUser()) { - profiles[conversation.id] - } else null + val conversationUser = VkUtils.getConversationUser(conversation, profiles) + val conversationGroup = VkUtils.getConversationGroup(conversation, groups) - val messageUser: VkUser? = if (message.isUser()) { - profiles[message.fromId] - } else null + val messageUser = VkUtils.getMessageUser(message, profiles) + val messageGroup = VkUtils.getMessageGroup(message, groups) - val chatGroup: VkGroup? = if (conversation.isGroup()) { - groups[conversation.id] - } else null - - val messageGroup: VkGroup? = if (message.isGroup()) { - groups[message.fromId] - } else null - - val avatar = when { - conversation.ownerId == VKConstants.FAST_GROUP_ID -> null - conversation.isUser() && chatUser != null && !chatUser.photo200.isNullOrBlank() -> chatUser.photo200 - conversation.isGroup() && chatGroup != null && !chatGroup.photo200.isNullOrBlank() -> chatGroup.photo200 - conversation.isChat() && !conversation.photo200.isNullOrBlank() -> conversation.photo200 - else -> null - } + val avatar = VkUtils.getConversationAvatar( + conversation = conversation, + conversationUser = conversationUser, + conversationGroup = conversationGroup + ) binding.avatar.isVisible = avatar != null @@ -136,7 +124,7 @@ class ConversationsAdapter constructor( } } - binding.online.isVisible = chatUser?.online == true + binding.online.isVisible = conversationUser?.online == true binding.pin.isVisible = conversation.isPinned @@ -210,7 +198,8 @@ class ConversationsAdapter constructor( binding.message.text = spanMessage binding.title.text = - getItem(position).title ?: chatUser?.toString() ?: chatGroup?.name ?: "..." + getItem(position).title ?: conversationUser?.toString() ?: conversationGroup?.name + ?: "..." binding.date.text = TimeUtils.getLocalizedTime(context, message.date * 1000L) @@ -232,6 +221,18 @@ class ConversationsAdapter constructor( } } + fun removeConversation(conversationId: Int): Int? { + for (i in values.indices) { + val conversation = values[i] + if (conversation.id == conversationId) { + values.removeAt(i) + return i + } + } + + return null + } + companion object { private val COMPARATOR = object : DiffUtil.ItemCallback() { override fun areItemsTheSame( diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt index a90f98f8..e63621a3 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt @@ -1,43 +1,46 @@ package com.meloda.fast.screens.conversations +import android.content.Intent import android.os.Bundle +import android.view.Gravity import android.view.View import android.viewbinding.library.fragment.viewBinding -import androidx.core.content.ContextCompat +import androidx.appcompat.widget.PopupMenu import androidx.core.os.bundleOf import androidx.core.view.isVisible +import androidx.core.view.updatePadding import androidx.datastore.preferences.core.edit import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import coil.load import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.snackbar.Snackbar +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.meloda.fast.R +import com.meloda.fast.activity.MainActivity import com.meloda.fast.api.UserConfig import com.meloda.fast.api.model.VkConversation import com.meloda.fast.base.BaseViewModelFragment import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent -import com.meloda.fast.base.viewmodel.VKEvent +import com.meloda.fast.base.viewmodel.VkEvent +import com.meloda.fast.common.AppGlobal import com.meloda.fast.common.AppSettings import com.meloda.fast.common.dataStore import com.meloda.fast.databinding.FragmentConversationsBinding import com.meloda.fast.util.AndroidUtils import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlin.math.abs +import kotlin.math.roundToInt @AndroidEntryPoint class ConversationsFragment : BaseViewModelFragment(R.layout.fragment_conversations) { - companion object { - const val TAG = "ConversationsFragment" - } - override val viewModel: ConversationsViewModel by viewModels() private val binding: FragmentConversationsBinding by viewBinding() @@ -53,8 +56,25 @@ class ConversationsFragment : } } + private val avatarPopupMenu: PopupMenu + get() = + PopupMenu( + requireContext(), + binding.avatar, + Gravity.BOTTOM + ).apply { + menu.add(getString(R.string.log_out)) + setOnMenuItemClickListener { item -> + if (item.title == getString(R.string.log_out)) { + showLogOutDialog() + return@setOnMenuItemClickListener true + } + + false + } + } + private var isPaused = false - private var isExpanded = true override fun onPause() { super.onPause() @@ -71,14 +91,10 @@ class ConversationsFragment : requireContext().dataStore.data.map { adapter.isMultilineEnabled = it[AppSettings.keyIsMultilineEnabled] ?: true adapter.notifyItemRangeChanged(0, adapter.itemCount) - }.collect { } + }.collect() } - binding.createChat.setOnClickListener { - Snackbar.make(it, "Test snackbar", Snackbar.LENGTH_SHORT) - .setAction("Action") {} - .show() - } + binding.createChat.setOnClickListener {} UserConfig.vkUser.observe(viewLifecycleOwner) { it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } } @@ -87,28 +103,32 @@ class ConversationsFragment : binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset -> if (isPaused) return@OnOffsetChangedListener - if (verticalOffset <= -100) { - binding.avatarContainer.alpha = 0f - return@OnOffsetChangedListener - } + binding.appBar.animate().translationZ( + if (verticalOffset < 0) AndroidUtils.px(3).roundToInt().toFloat() + else 0f + ).setDuration(50).start() - val alpha = 1 - abs(verticalOffset * 0.01).toFloat() + val padding = AndroidUtils.px(if (verticalOffset <= -100) 10 else 30).roundToInt() + + binding.avatarContainer.updatePadding( + bottom = padding, + right = padding + ) + + val minusAlpha = (1 - (abs(verticalOffset) * 0.01)).toFloat() + val plusAlpha = (abs(1 + verticalOffset * 0.01) * 1.01).toFloat() + + println("Fast::ConversationsFragment::onOffset offset: $verticalOffset; minusAlpha: $minusAlpha; plusAlpha: $plusAlpha") + + val alpha: Float = if (verticalOffset <= -100) plusAlpha else minusAlpha binding.avatarContainer.alpha = alpha }) - if (isPaused) { - isPaused = false - return - } + binding.avatar.setOnClickListener { avatarPopupMenu.show() } - binding.toolbar.overflowIcon = ContextCompat.getDrawable(requireContext(), R.drawable.test) - - viewModel.loadProfileUser() - viewModel.loadConversations() - - binding.avatar.setOnClickListener { - lifecycleScope.launchWhenResumed { + binding.avatar.setOnLongClickListener { + lifecycleScope.launch { requireContext().dataStore.edit { settings -> val isMultilineEnabled = settings[AppSettings.keyIsMultilineEnabled] ?: true settings[AppSettings.keyIsMultilineEnabled] = !isMultilineEnabled @@ -117,15 +137,59 @@ class ConversationsFragment : adapter.notifyItemRangeChanged(0, adapter.itemCount) } } + true } + + if (isPaused) { + isPaused = false + return + } + + viewModel.loadProfileUser() + viewModel.loadConversations() } - override fun onEvent(event: VKEvent) { + private fun showLogOutDialog() { + val isEasterEgg = UserConfig.userId == UserConfig.userId + + MaterialAlertDialogBuilder(requireContext()) + .setTitle( + if (isEasterEgg) "Выйти внаружу?" + else getString(R.string.sign_out_confirm_title) + ) + .setMessage(R.string.sign_out_confirm) + .setPositiveButton( + if (isEasterEgg) "Выйти внаружу" + else getString(R.string.action_sign_out) + ) { _, _ -> + lifecycleScope.launch(Dispatchers.Default) { + UserConfig.clear() + AppGlobal.appDatabase.clearAllTables() + + requireActivity().finishAffinity() + requireActivity().startActivity( + Intent( + requireContext(), + MainActivity::class.java + ) + ) + } + } + .setNegativeButton(R.string.no, null) + .show() + } + + override fun onEvent(event: VkEvent) { super.onEvent(event) when (event) { - is ConversationsLoaded -> refreshConversations(event) is StartProgressEvent -> onProgressStarted() is StopProgressEvent -> onProgressStopped() + + is ConversationsLoaded -> refreshConversations(event) + is ConversationsDelete -> deleteConversation(event.peerId) + + // TODO: 10-Oct-21 remove this and sort conversations list + is ConversationsPin, is ConversationsUnpin -> viewModel.loadConversations() } } @@ -179,13 +243,19 @@ class ConversationsFragment : private fun fillRecyclerView(values: List) { adapter.values.clear() adapter.values += values - adapter.notifyItemRangeChanged(0, adapter.itemCount) + adapter.submitList(values) } private fun onItemClick(position: Int) { val conversation = adapter[position] - val user = if (conversation.isUser()) adapter.profiles[conversation.id] else null - val group = if (conversation.isGroup()) adapter.groups[conversation.id] else null + + val user = + if (conversation.isUser()) adapter.profiles[conversation.id] + else null + + val group = + if (conversation.isGroup()) adapter.groups[conversation.id] + else null findNavController().navigate( R.id.toMessagesHistory, @@ -198,8 +268,81 @@ class ConversationsFragment : } private fun onItemLongClick(position: Int): Boolean { - binding.createChat.performClick() + showOptionsDialog(position) return true } + private fun showOptionsDialog(position: Int) { + val conversation = adapter[position] + + var canPinOneMoreDialog = true + if (adapter.itemCount > 4) { + val firstFiveDialogs = adapter.values.subList(0, 5) + var pinnedCount = 0 + + firstFiveDialogs.forEach { if (it.isPinned) pinnedCount++ } + if (pinnedCount == 5 && position > 4) { + canPinOneMoreDialog = false + } + } + + val pin = getString( + if (conversation.isPinned) R.string.conversation_context_action_unpin + else R.string.conversation_context_action_pin + ) + + val delete = getString(R.string.conversation_context_action_delete) + + val params = mutableListOf() + + if (canPinOneMoreDialog) params += pin + + params += delete + + val arrayParams = params.toTypedArray() + + MaterialAlertDialogBuilder(requireContext()) + .setItems(arrayParams) { _, which -> + when (params[which]) { + pin -> showPinConversationDialog(conversation) + delete -> showDeleteConversationDialog(conversation.id) + } + }.show() + } + + private fun showDeleteConversationDialog(conversationId: Int) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.confirm_delete_conversation) + .setPositiveButton(R.string.action_delete) { _, _ -> + viewModel.deleteConversation(conversationId) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun deleteConversation(conversationId: Int) { + val index = adapter.removeConversation(conversationId) ?: return + adapter.notifyItemRemoved(index) + } + + private fun showPinConversationDialog(conversation: VkConversation) { + val isPinned = conversation.isPinned + MaterialAlertDialogBuilder(requireContext()) + .setTitle( + if (isPinned) R.string.confirm_unpin_conversation + else R.string.confirm_pin_conversation + ) + .setPositiveButton( + if (isPinned) R.string.action_unpin + else R.string.action_pin + ) { _, _ -> + viewModel.pinConversation( + peerId = conversation.id, + pin = !isPinned + ) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt index d4d9209b..c750edd0 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt @@ -3,17 +3,14 @@ package com.meloda.fast.screens.conversations import androidx.lifecycle.viewModelScope import com.meloda.fast.api.UserConfig import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.network.datasource.ConversationsDataSource -import com.meloda.fast.api.network.datasource.UsersDataSource import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.request.ConversationsGetRequest -import com.meloda.fast.api.model.request.UsersGetRequest +import com.meloda.fast.api.network.conversations.* +import com.meloda.fast.api.network.users.UsersDataSource +import com.meloda.fast.api.network.users.UsersGetRequest import com.meloda.fast.base.viewmodel.BaseViewModel -import com.meloda.fast.base.viewmodel.StartProgressEvent -import com.meloda.fast.base.viewmodel.StopProgressEvent -import com.meloda.fast.base.viewmodel.VKEvent +import com.meloda.fast.base.viewmodel.VkEvent import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -22,18 +19,20 @@ import javax.inject.Inject @HiltViewModel class ConversationsViewModel @Inject constructor( - private val dataSource: ConversationsDataSource, - private val usersDataSource: UsersDataSource + private val conversations: ConversationsDataSource, + private val users: UsersDataSource ) : BaseViewModel() { - fun loadConversations() = viewModelScope.launch(Dispatchers.Default) { + fun loadConversations( + offset: Int? = null + ) = viewModelScope.launch(Dispatchers.Default) { makeJob({ - dataSource.getAllChats( + conversations.get( ConversationsGetRequest( count = 30, -// offset = 177, extended = true, - fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}" + offset = offset, + fields = VKConstants.ALL_FIELDS ) ) }, @@ -52,6 +51,7 @@ class ConversationsViewModel @Inject constructor( sendEvent( ConversationsLoaded( count = response.count, + offset = offset, unreadCount = response.unreadCount ?: 0, conversations = response.items.map { items -> items.conversation.asVkConversation( @@ -63,34 +63,59 @@ class ConversationsViewModel @Inject constructor( ) ) } - }, - onError = { - val er = it - throw it - }, - onStart = { sendEvent(StartProgressEvent) }, - onEnd = { sendEvent(StopProgressEvent) }) + } + ) } fun loadProfileUser() = viewModelScope.launch { - makeJob({ - usersDataSource.getById(UsersGetRequest(fields = "online,photo_200")) - }, + makeJob({ users.getById(UsersGetRequest(fields = VKConstants.USER_FIELDS)) }, onAnswer = { it.response?.let { r -> val users = r.map { u -> u.asVkUser() } - usersDataSource.storeUsers(users) + this@ConversationsViewModel.users.storeUsers(users) UserConfig.vkUser.value = users[0] } }) } + + fun deleteConversation(peerId: Int) = viewModelScope.launch { + makeJob({ + conversations.delete( + ConversationsDeleteRequest(peerId) + ) + }, onAnswer = { sendEvent(ConversationsDelete(peerId)) }) + } + + fun pinConversation( + peerId: Int, + pin: Boolean + ) = viewModelScope.launch { + if (pin) { + makeJob( + { conversations.pin(ConversationsPinRequest(peerId)) }, + onAnswer = { sendEvent(ConversationsPin(peerId)) } + ) + } else { + makeJob( + { conversations.unpin(ConversationsUnpinRequest(peerId)) }, + onAnswer = { sendEvent(ConversationsUnpin(peerId)) } + ) + } + } } data class ConversationsLoaded( val count: Int, + val offset: Int?, val unreadCount: Int?, val conversations: List, val profiles: HashMap, val groups: HashMap -) : VKEvent() +) : VkEvent() + +data class ConversationsDelete(val peerId: Int) : VkEvent() + +data class ConversationsPin(val peerId: Int) : VkEvent() + +data class ConversationsUnpin(val peerId: Int) : VkEvent() \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt index 7893cec7..d808d711 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt @@ -1,11 +1,17 @@ package com.meloda.fast.screens.login +import android.annotation.SuppressLint +import android.graphics.Bitmap import android.graphics.Typeface import android.os.Bundle +import android.util.Log import android.view.KeyEvent import android.view.View import android.view.inputmethod.EditorInfo import android.viewbinding.library.fragment.viewBinding +import android.webkit.CookieManager +import android.webkit.WebView +import android.webkit.WebViewClient import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible @@ -19,6 +25,8 @@ import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputLayout import com.meloda.fast.BuildConfig import com.meloda.fast.R +import com.meloda.fast.api.UserConfig +import com.meloda.fast.api.VKConstants import com.meloda.fast.base.BaseViewModelFragment import com.meloda.fast.base.viewmodel.* import com.meloda.fast.databinding.DialogCaptchaBinding @@ -29,7 +37,10 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.jsoup.Jsoup +import java.net.URLEncoder import java.util.* +import java.util.regex.Pattern import kotlin.concurrent.schedule @AndroidEntryPoint @@ -59,14 +70,14 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo binding.loginInput.clearFocus() } - override fun onEvent(event: VKEvent) { + override fun onEvent(event: VkEvent) { super.onEvent(event) when (event) { - is ShowError -> showErrorSnackbar(event.errorDescription) + is ErrorEvent -> showErrorSnackbar(event.errorText) is CaptchaEvent -> showCaptchaDialog(event.sid, event.image) is ValidationEvent -> showValidationRequired(event.sid) - is SuccessAuth -> goToMain(event.haveAuthorized) + is SuccessAuth -> goToMain(event) is CodeSent -> showValidationDialog() is StartProgressEvent -> onProgressStarted() @@ -89,11 +100,91 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo } private fun prepareViews() { + prepareWebView() prepareEmailEditText() preparePasswordEditText() prepareAuthButton() } + @SuppressLint("SetJavaScriptEnabled") + private fun prepareWebView() { + with(binding.webView) { + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + + clearCache(true) + webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + parseAuthUrl(url) + } + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + + val a = Jsoup.parse(url) + + val b = 0 + } + } + } + + CookieManager.getInstance().apply { + removeAllCookies(null) + flush() + setAcceptCookie(true) + } + } + + private fun launchWebView() { + binding.webView.loadUrl( + "https://oauth.vk.com/authorize?client_id=${UserConfig.FAST_APP_ID}&" + + "display=mobile&scope=136297695&" + + "redirect_uri=${ + URLEncoder.encode( + "https://oauth.vk.com/blank.html", + Charsets.UTF_8.toString() + ) + }&response_type=token&v=${VKConstants.API_VERSION}" + ) + } + + private fun parseAuthUrl(url: String) { + if (url.isBlank()) return + + if (url.startsWith("https://oauth.vk.com/blank.html")) { + if (url.contains("error")) { + Log.e("Fast::Login", "errorUrl: $url") + return + } + + val authData = parseRedirectUrl(url) + if (authData == null) { + Log.e("Fast::Login", "errorUrl: $url") + return + } + + val token = authData.first + + UserConfig.fastToken = token + } + } + + private fun parseRedirectUrl(url: String): Pair? { + val accessToken = extractPattern(url, "access_token=(.*?)&") ?: return null + val userId = extractPattern(url, "id=(\\d*)")?.toIntOrNull() ?: return null + + return accessToken to userId + } + + private fun extractPattern(string: String, pattern: String): String? { + val p = Pattern.compile(pattern) + val m = p.matcher(string) + return if (m.find()) { + m.group(1) + } else null + } + private fun prepareEmailEditText() { binding.loginInput.addTextChangedListener { if (!binding.loginLayout.error.isNullOrBlank()) binding.loginLayout.error = "" @@ -293,8 +384,13 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo snackbar.show() } - private fun goToMain(haveAuthorized: Boolean) = lifecycleScope.launch { - if (haveAuthorized) delay(500) + private fun goToMain(event: SuccessAuth) = lifecycleScope.launch { + UserConfig.userId = event.userId + UserConfig.accessToken = event.vkToken + + if (event.haveAuthorized) delay(500) + + launchWebView() findNavController().navigate(R.id.toMain) } diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt index c4a613e1..c4666d32 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt @@ -1,11 +1,10 @@ package com.meloda.fast.screens.login import androidx.lifecycle.viewModelScope -import com.meloda.fast.api.UserConfig import com.meloda.fast.api.VKConstants import com.meloda.fast.api.VKException -import com.meloda.fast.api.model.request.RequestAuthDirect -import com.meloda.fast.api.network.datasource.AuthDataSource +import com.meloda.fast.api.network.auth.AuthDataSource +import com.meloda.fast.api.network.auth.RequestAuthDirect import com.meloda.fast.base.viewmodel.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -45,33 +44,37 @@ class LoginViewModel @Inject constructor( return@makeJob } - UserConfig.userId = it.userId - UserConfig.accessToken = it.accessToken - - sendEvent(SuccessAuth()) + sendEvent( + SuccessAuth( + userId = it.userId, + vkToken = it.accessToken + ) + ) }, onError = { - if (it !is VKException) return@makeJob + if (it !is VKException) { + onError(it) + return@makeJob + } + // TODO: 9/27/2021 use `delay` parameter twoFaCode?.let { sendEvent(CodeSent) } - }, - onStart = { sendEvent(StartProgressEvent) }, - onEnd = { sendEvent(StopProgressEvent) } + } ) } fun sendSms(validationSid: String) = viewModelScope.launch { makeJob({ dataSource.sendSms(validationSid) }, - onAnswer = { sendEvent(CodeSent) }, - onError = {}, - onStart = {}, - onEnd = {}) + onAnswer = { sendEvent(CodeSent) } + ) } } -data class ShowError(val errorDescription: String) : VKEvent() +object CodeSent : VkEvent() -object CodeSent : VKEvent() - -data class SuccessAuth(val haveAuthorized: Boolean = true) : VKEvent() \ No newline at end of file +data class SuccessAuth( + val haveAuthorized: Boolean = true, + val userId: Int, + val vkToken: String +) : VkEvent() \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt index 58f5fcf9..980c9f8f 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt @@ -42,5 +42,4 @@ class MainFragment : BaseViewModelFragment(R.layout.fragment_main } } - } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt index 581ec9a3..b0b7a332 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt @@ -15,9 +15,11 @@ import androidx.core.content.ContextCompat import androidx.core.view.isNotEmpty import androidx.core.view.isVisible import androidx.core.view.setPadding +import androidx.core.view.updatePadding import coil.load import com.google.android.material.imageview.ShapeableImageView import com.meloda.fast.R +import com.meloda.fast.api.UserConfig import com.meloda.fast.api.VkUtils import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkMessage @@ -30,9 +32,11 @@ import java.text.SimpleDateFormat import java.util.* import kotlin.math.roundToInt +// TODO: 9/29/2021 use recyclerview for viewing attachments class AttachmentInflater constructor( private val context: Context, private val container: LinearLayoutCompat, + private val textContainer: LinearLayoutCompat, private val message: VkMessage, private val profiles: Map, private val groups: Map @@ -44,16 +48,28 @@ class AttachmentInflater constructor( private val playColor = ContextCompat.getColor(context, R.color.a3_700) private val playBackgroundColor = ContextCompat.getColor(context, R.color.a3_200) + var photoClickListener: ((url: String) -> Unit)? = null + + fun setPhotoClickListener(unit: ((url: String) -> Unit)?): AttachmentInflater { + this.photoClickListener = unit + return this + } + fun inflate() { if (message.attachments.isNullOrEmpty()) return attachments = message.attachments!! container.removeAllViews() + textContainer.removeAllViews() if (attachments.size == 1) { when (val attachment = attachments[0]) { is VkSticker -> return sticker(attachment) is VkWall -> return wall(attachment) + is VkVoiceMessage -> return voice(attachment) + is VkCall -> return call(attachment) + is VkGraffiti -> return graffiti(attachment) + is VkGift -> return gift(attachment) } } @@ -82,7 +98,6 @@ class AttachmentInflater constructor( is VkAudio -> audio(attachment) is VkFile -> file(attachment) is VkLink -> link(attachment) - is VkStory -> story(attachment) else -> Log.e( "Attachment inflater", @@ -94,12 +109,15 @@ class AttachmentInflater constructor( } private fun photo(photo: VkPhoto) { - val size = photo.sizeOfType('m') ?: return + val size = photo.getSizeOrSmaller('y') ?: return val newPhoto = ShapeableImageView(context).apply { layoutParams = LinearLayoutCompat.LayoutParams( - AndroidUtils.px(size.width).roundToInt(), - AndroidUtils.px(size.height).roundToInt() +// ViewGroup.LayoutParams.MATCH_PARENT, + size.width, + size.height +// AndroidUtils.px(size.width).roundToInt(), +// AndroidUtils.px(size.height).roundToInt() ) shapeAppearanceModel = @@ -110,6 +128,12 @@ class AttachmentInflater constructor( scaleType = ImageView.ScaleType.CENTER_CROP } + if (photoClickListener != null) { + newPhoto.setOnClickListener { photoClickListener?.invoke(size.url) } + } else { + newPhoto.setOnClickListener(null) + } + val spacer = Space(context).also { it.layoutParams = LinearLayoutCompat.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -222,14 +246,7 @@ class AttachmentInflater constructor( 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 { + link.photo?.getMaxSize()?.let { binding.preview.load(it.url) { crossfade(150) } binding.preview.isVisible = true return @@ -245,8 +262,8 @@ class AttachmentInflater constructor( with(binding.image) { layoutParams = LinearLayoutCompat.LayoutParams( - AndroidUtils.px(180).roundToInt(), - AndroidUtils.px(180).roundToInt() + AndroidUtils.px(140).roundToInt(), + AndroidUtils.px(140).roundToInt() ) load(url) { crossfade(150) } @@ -282,7 +299,7 @@ class AttachmentInflater constructor( binding.avatar.isVisible = group != null || user != null binding.avatar.shapeAppearanceModel.toBuilder() - .setAllCornerSizes(40f) + .setAllCornerSizes(AndroidUtils.px(20)) .build() .let { binding.avatar.shapeAppearanceModel = it @@ -302,8 +319,91 @@ class AttachmentInflater constructor( ).format(wall.date * 1000L) } - private fun story(story: VkStory) { + private fun voice(voiceMessage: VkVoiceMessage) { + val binding = ItemMessageAttachmentVoiceBinding.inflate(inflater, textContainer, true) + if (message.isOut) + binding.root.updatePadding( + bottom = AndroidUtils.px(6).roundToInt(), + left = AndroidUtils.px(6).roundToInt() + ) + + val waveform = IntArray(voiceMessage.waveform.size) + voiceMessage.waveform.forEachIndexed { index, i -> waveform[index] = i } + + binding.waveform.sample = waveform + binding.waveform.maxProgress = 100f + binding.waveform.progress = 100f + + binding.duration.text = SimpleDateFormat( + "mm:ss", + Locale.getDefault() + ).format(voiceMessage.duration * 1000L) + } + + private fun call(call: VkCall) { + val binding = ItemMessageAttachmentCallBinding.inflate(inflater, textContainer, true) + + if (message.isOut) + binding.root.updatePadding( + bottom = AndroidUtils.px(5).roundToInt(), + left = AndroidUtils.px(6).roundToInt() + ) + + val callType = + context.getString( + if (call.initiatorId == UserConfig.userId) R.string.message_call_type_outgoing + else R.string.message_call_type_incoming + ) + + binding.type.text = callType + + var callState = + context.getString( + if (call.state == "reached") R.string.message_call_state_ended + else if (call.state == "canceled_by_initiator") { + if (call.initiatorId == UserConfig.userId) R.string.message_call_state_cancelled + else R.string.message_call_state_missed + } else R.string.message_call_unknown + ) + + if (callState == context.getString(R.string.message_call_unknown)) callState = call.state + + binding.state.text = callState + } + + private fun graffiti(graffiti: VkGraffiti) { + val binding = ItemMessageAttachmentGraffitiBinding.inflate(inflater, container, true) + + val url = graffiti.url + + val heightCoefficient = graffiti.height / AndroidUtils.px(140) + + with(binding.image) { + layoutParams = LinearLayoutCompat.LayoutParams( + AndroidUtils.px(140).roundToInt(), + (graffiti.height / heightCoefficient).roundToInt() + ) + + load(url) { crossfade(150) } + } + } + + private fun gift(gift: VkGift) { + val binding = ItemMessageAttachmentGiftBinding.inflate(inflater, container, true) + + val url = gift.thumb256 ?: gift.thumb96 ?: gift.thumb48 + + with(binding.image) { + shapeAppearanceModel = shapeAppearanceModel.withCornerSize { AndroidUtils.px(12) } + + layoutParams = LinearLayoutCompat.LayoutParams( + AndroidUtils.px(140).roundToInt(), + AndroidUtils.px(140).roundToInt() + ) + + load(url) { crossfade(150) } + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt index c296733b..038b4a25 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt @@ -5,6 +5,7 @@ import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.appcompat.widget.LinearLayoutCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil @@ -29,7 +30,9 @@ class MessagesHistoryAdapter constructor( val conversation: VkConversation, val profiles: HashMap = hashMapOf(), val groups: HashMap = hashMapOf() -) : BaseAdapter(context, values, COMPARATOR) { +) : BaseAdapter(context, values, COMPARATOR) { + + var avatarLongClickListener: ((position: Int) -> Unit)? = null override fun getItemViewType(position: Int): Int { when { @@ -49,7 +52,7 @@ class MessagesHistoryAdapter constructor( private fun isPositionHeader(position: Int) = position == 0 private fun isPositionFooter(position: Int) = position >= actualSize - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicHolder { return when (viewType) { // magick numbers is great! HEADER -> Header(createEmptyView(60)) @@ -67,6 +70,21 @@ class MessagesHistoryAdapter constructor( } } +// override fun initListeners(itemView: View, position: Int) { +// if (itemView is AdapterView<*>) return +// +// itemView.setOnClickListener { onItemClickListener?.invoke(position, itemView) } +// itemView.setOnLongClickListener { itemLongClickListener.invoke(position) } +// } + + + val actualSize get() = values.size + + override fun getItemCount(): Int { + if (actualSize == 0) return 2 + return super.getItemCount() + 2 + } + private fun createEmptyView(size: Int) = View(context).apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -78,22 +96,22 @@ class MessagesHistoryAdapter constructor( isFocusable = false } - override fun onBindViewHolder(holder: Holder, position: Int) { + override fun onBindViewHolder(holder: BasicHolder, position: Int) { if (holder is Header || holder is Footer) return initListeners(holder.itemView, position) holder.bind(position) } - open inner class Holder(v: View = View(context)) : BaseHolder(v) + open inner class BasicHolder(v: View = View(context)) : BaseHolder(v) - inner class Header(v: View) : Holder(v) + inner class Header(v: View) : BasicHolder(v) - inner class Footer(v: View) : Holder(v) + inner class Footer(v: View) : BasicHolder(v) inner class IncomingMessage( private val binding: ItemMessageInBinding - ) : Holder(binding.root) { + ) : BasicHolder(binding.root) { override fun bind(position: Int) { val message = getItem(position) @@ -103,33 +121,42 @@ class MessagesHistoryAdapter constructor( MessagesPreparator( context = context, + + root = binding.root, + conversation = conversation, message = message, prevMessage = prevMessage, nextMessage = nextMessage, + title = binding.title, + avatar = binding.avatar, bubble = binding.bubble, text = binding.text, spacer = binding.spacer, - time = binding.time, unread = binding.unread, + + textContainer = binding.textContainer, attachmentContainer = binding.attachmentContainer, attachmentSpacer = binding.attachmentSpacer, profiles = profiles, groups = groups - ).prepare() + ).setPhotoClickListener { + Toast.makeText(context, "Photo url: $it", Toast.LENGTH_LONG).show() + }.prepare() + + binding.avatar.setOnLongClickListener() { + avatarLongClickListener?.invoke(position) + true + } } } inner class OutgoingMessage( private val binding: ItemMessageOutBinding - ) : Holder(binding.root) { - - init { - binding.bubbleStroke.setOnClickListener { binding.bubble.performClick() } - } + ) : BasicHolder(binding.root) { override fun bind(position: Int) { val message = getItem(position) @@ -138,16 +165,17 @@ class MessagesHistoryAdapter constructor( MessagesPreparator( context = context, + root = binding.root, conversation = conversation, message = message, prevMessage = prevMessage, bubble = binding.bubble, - bubbleStroke = binding.bubbleStroke, text = binding.text, spacer = binding.spacer, - time = binding.time, unread = binding.unread, + + textContainer = binding.textContainer, attachmentContainer = binding.attachmentContainer, attachmentSpacer = binding.attachmentSpacer, @@ -159,7 +187,7 @@ class MessagesHistoryAdapter constructor( inner class ServiceMessage( private val binding: ItemMessageServiceBinding - ) : Holder(binding.root) { + ) : BasicHolder(binding.root) { private val youPrefix = context.getString(R.string.you_message_prefix) @@ -198,7 +226,7 @@ class MessagesHistoryAdapter constructor( binding.photo.isVisible = true - val size = attachment.sizeOfType('m') ?: return@let + val size = attachment.getSizeOrSmaller('y') ?: return@let binding.photo.layoutParams = LinearLayoutCompat.LayoutParams( size.width, @@ -213,11 +241,39 @@ class MessagesHistoryAdapter constructor( } } - private val actualSize get() = values.size + fun removeMessageById(id: Int): Int? { + for (i in values.indices) { + val message = values[i] + if (message.id == id) { + values.removeAt(i) + return i + } + } - override fun getItemCount(): Int { - if (actualSize == 0) return 2 - return super.getItemCount() + 2 + return null + } + + fun removeMessagesByIds(ids: List): List { + val positions = mutableListOf() + + for (i in values.indices) { + val message = values[i] + if (ids.contains(message.id)) { + values.removeAt(i) + positions += i + } + } + + return positions + } + + fun searchMessageIndex(messageId: Int): Int? { + for (i in values.indices) { + val message = values[i] + if (message.id == messageId) return i + } + + return null } companion object { diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt index 5c9be18b..b4242f11 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt @@ -1,12 +1,16 @@ package com.meloda.fast.screens.messages -import android.graphics.Color +import android.content.res.ColorStateList import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.text.TextUtils import android.view.View import android.viewbinding.library.fragment.viewBinding +import android.widget.Toast +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.core.view.setPadding import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.viewModels import androidx.lifecycle.MutableLiveData @@ -16,6 +20,8 @@ import coil.load import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.meloda.fast.R import com.meloda.fast.api.UserConfig +import com.meloda.fast.api.VKConstants +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 @@ -23,7 +29,8 @@ import com.meloda.fast.api.model.VkUser import com.meloda.fast.base.BaseViewModelFragment import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent -import com.meloda.fast.base.viewmodel.VKEvent +import com.meloda.fast.base.viewmodel.VkEvent +import com.meloda.fast.databinding.DialogMessageDeleteBinding import com.meloda.fast.databinding.FragmentMessagesHistoryBinding import com.meloda.fast.extensions.TextViewExtensions.clear import com.meloda.fast.util.AndroidUtils @@ -32,6 +39,7 @@ import dagger.hilt.android.AndroidEntryPoint import java.text.SimpleDateFormat import java.util.* import kotlin.concurrent.schedule +import kotlin.math.roundToInt @AndroidEntryPoint class MessagesHistoryFragment : @@ -43,7 +51,7 @@ class MessagesHistoryFragment : private val action = MutableLiveData() private enum class Action { - RECORD, SEND + RECORD, SEND, EDIT, DELETE } private val user: VkUser? by lazy { @@ -62,14 +70,19 @@ class MessagesHistoryFragment : MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also { it.itemClickListener = this::onItemClick it.itemLongClickListener = this::onItemLongClick + it.avatarLongClickListener = this::onAvatarLongClickListener } } private var timestampTimer: Timer? = null + private lateinit var attachmentController: AttachmentPanelController + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + attachmentController = AttachmentPanelController().init() + val title = when { conversation.isChat() -> conversation.title conversation.isUser() -> user?.toString() @@ -98,19 +111,7 @@ class MessagesHistoryFragment : binding.status.text = status ?: "..." - val avatar = when { - conversation.isChat() -> conversation.photo200 - conversation.isUser() -> user?.photo200 - conversation.isGroup() -> group?.photo200 - else -> null - } - - binding.avatar.load(avatar) { - crossfade(false) - error(ColorDrawable(Color.RED)) - } - - binding.online.isVisible = user?.online == true + prepareAvatar() prepareViews() @@ -166,8 +167,14 @@ class MessagesHistoryFragment : }) binding.message.doAfterTextChanged { - val newValue = if (it.toString().isNotBlank()) Action.SEND - else Action.RECORD + val canSend = it.toString().isNotBlank() + + val newValue: Action = + when { + attachmentController.isEditing -> if (it.isNullOrBlank()) Action.DELETE else Action.EDIT + canSend -> Action.SEND + else -> Action.RECORD + } if (action.value != newValue) action.value = newValue } @@ -192,52 +199,159 @@ class MessagesHistoryFragment : Action.SEND -> { binding.action.setImageResource(R.drawable.ic_round_send_24) } + Action.EDIT -> { + binding.action.setImageResource(R.drawable.ic_round_done_24) + } + Action.DELETE -> { + binding.action.setImageResource(R.drawable.ic_trash_can_outline_24) + } else -> return@observe } } + + attachmentController.isPanelVisible.observe(viewLifecycleOwner) { + if (it) binding.message.setSelection(binding.message.text.toString().length) + + val layoutParams = binding.refreshLayout.layoutParams as CoordinatorLayout.LayoutParams + layoutParams.bottomMargin = + if (it) (binding.attachmentPanel.height / 1.5).roundToInt() else 0 + } + + binding.attachmentPanel.setOnClickListener c@{ + val message = attachmentController.message.value ?: return@c + + val index = adapter.values.indexOf(message) + if (index == -1) return@c + + binding.recyclerView.smoothScrollToPosition(index) + } + + binding.dismissReply.setOnClickListener { + if (attachmentController.message.value != null) + attachmentController.message.value = null + } + } + + private fun prepareAvatar() { + val avatar = when { + conversation.ownerId == VKConstants.FAST_GROUP_ID -> null + conversation.isUser() -> user?.photo200 + conversation.isGroup() -> group?.photo200 + conversation.isChat() -> conversation.photo200 + else -> null + } + + binding.avatar.isVisible = avatar != null + + if (avatar == null) { + binding.avatarPlaceholder.isVisible = true + + if (conversation.ownerId == VKConstants.FAST_GROUP_ID) { + binding.placeholderBack.setImageDrawable( + ColorDrawable( + ContextCompat.getColor(requireContext(), R.color.a1_400) + ) + ) + binding.placeholder.imageTintList = + ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.a1_0)) + binding.placeholder.setImageResource(R.drawable.ic_fast_logo) + binding.placeholder.setPadding(18) + } else { + binding.placeholderBack.setImageDrawable( + ColorDrawable( + ContextCompat.getColor(requireContext(), R.color.n1_50) + ) + ) + binding.placeholder.imageTintList = + ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.n2_500)) + binding.placeholder.setImageResource(R.drawable.ic_account_circle_cut) + binding.placeholder.setPadding(0) + binding.avatar.setImageDrawable(null) + } + } else { + binding.avatar.load(avatar) { + crossfade(200) + target { + binding.avatarPlaceholder.isVisible = false + binding.avatar.setImageDrawable(it) + } + } + } + + binding.phantomIcon.isVisible = conversation.isPhantom + binding.online.isVisible = user?.online == true + binding.pin.isVisible = conversation.isPinned + } + + private fun performAction() { + when (action.value) { + Action.RECORD -> { + } + Action.SEND -> { + val messageText = binding.message.text.toString().trim() + if (messageText.isBlank()) return + + val date = System.currentTimeMillis() + + val message = VkMessage( + id = -1, + text = messageText, + isOut = true, + peerId = conversation.id, + fromId = UserConfig.userId, + date = (date / 1000).toInt(), + randomId = 0, + replyMessage = attachmentController.message.value + ) + + adapter.add(message) + adapter.notifyItemInserted(adapter.actualSize - 1) + binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) + binding.message.clear() + + val replyMessage = attachmentController.message.value + attachmentController.message.value = null + + viewModel.sendMessage( + peerId = conversation.id, + message = messageText, + randomId = 0, + replyTo = replyMessage?.id + ) { message.id = it } + } + Action.EDIT -> { + val message = attachmentController.message.value ?: return + val messageText = binding.message.text.toString().trim() + + attachmentController.message.value = null + + viewModel.editMessage( + originalMessage = message, + peerId = conversation.id, + messageId = message.id, + message = messageText, + attachments = message.attachments + ) + } + Action.DELETE -> attachmentController.message.value?.let { + showDeleteMessageDialog(it) + } + } } - - private fun performAction() { - if (action.value == Action.RECORD) { - - } else if (action.value == Action.SEND) { - val messageText = binding.message.text.toString().trim() - if (messageText.isBlank()) return - - val date = System.currentTimeMillis() - - var message = VkMessage( - id = -1, - text = messageText, - isOut = true, - peerId = conversation.id, - fromId = UserConfig.userId, - date = (date / 1000).toInt(), - randomId = 0 - ) - - adapter.add(message) - adapter.notifyDataSetChanged() - binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) - binding.message.clear() - - viewModel.sendMessage( - peerId = conversation.id, - message = messageText, - randomId = 0 - ) { message = message.copyMessage(id = it) } - } - } - - override fun onEvent(event: VKEvent) { + override fun onEvent(event: VkEvent) { super.onEvent(event) when (event) { - is MessagesMarkAsImportant -> markMessagesAsImportant(event) - is MessagesLoaded -> refreshMessages(event) is StartProgressEvent -> onProgressStarted() is StopProgressEvent -> onProgressStopped() + + is MessagesMarkAsImportant -> markMessagesAsImportant(event) + is MessagesLoaded -> refreshMessages(event) + is MessagesPin -> conversation.pinnedMessage = event.message + is MessagesUnpin -> conversation.pinnedMessage = null + is MessagesDelete -> deleteMessages(event) + is MessagesEdit -> editMessage(event) } } @@ -283,17 +397,21 @@ class MessagesHistoryFragment : private fun markMessagesAsImportant(event: MessagesMarkAsImportant) { var changed = false + val positions = mutableListOf() + for (i in adapter.values.indices) { val message = adapter.values[i] + message.important = event.important if (event.messagesIds.contains(message.id)) { if (!changed) changed = true - adapter.values[i] = message.copyMessage( - important = event.important - ) + + positions.add(i) + + adapter.values[i] = message } } - if (changed) adapter.notifyDataSetChanged() + if (changed) positions.forEach { adapter.notifyItemChanged(it) } } private fun refreshMessages(event: MessagesLoaded) { @@ -315,30 +433,235 @@ class MessagesHistoryFragment : } private fun onItemClick(position: Int) { + showOptionsDialog(position) + } + + private fun onItemLongClick(position: Int) = true + + private fun onAvatarLongClickListener(position: Int) { + val message = adapter.values[position] + + val messageUser = VkUtils.getMessageUser(message, adapter.profiles) + val messageGroup = VkUtils.getMessageGroup(message, adapter.groups) + + val title = VkUtils.getMessageTitle(message, messageUser, messageGroup) + Toast.makeText(requireContext(), title, Toast.LENGTH_SHORT).show() + } + + private fun showOptionsDialog(position: Int) { val message = adapter.values[position] if (message.action != null) return - val important = if (message.important) "Unmark as important" else "Mark as important" + val time = getString( + R.string.time_format, + SimpleDateFormat( + "dd.MM.yyyy, HH:mm:ss", + Locale.getDefault() + ).format(message.date * 1000L) + ) - val params = arrayOf(important) + val important = getString( + if (message.important) R.string.message_context_action_unmark_as_important + else R.string.message_context_action_mark_as_important + ) - val dialog = MaterialAlertDialogBuilder(requireContext()) - .setItems(params) { _, which -> - if (which == 0) { - viewModel.markAsImportant( + val reply = getString(R.string.message_context_action_reply) + + val isMessageAlreadyPinned = message.id == conversation.pinnedMessage?.id + + val pin = getString( + if (isMessageAlreadyPinned) R.string.message_context_action_unpin + else R.string.message_context_action_pin + ) + + val edit = getString(R.string.message_context_action_edit) + + val delete = getString(R.string.message_context_action_delete) + + val params = mutableListOf( + important, reply + ) + + if (conversation.canChangePin) { + params += pin + } + + if (message.canEdit()) { + params += edit + } + + params += delete + + val arrayParams = params.toTypedArray() + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(time) + .setItems(arrayParams) { _, which -> + when (params[which]) { + important -> viewModel.markAsImportant( messagesIds = listOf(message.id), important = !message.important ) + reply -> { + if (attachmentController.message.value != message) + attachmentController.message.value = message + } + pin -> + showPinMessageDialog( + peerId = conversation.id, + messageId = message.id, + pin = !isMessageAlreadyPinned + ) + edit -> { + attachmentController.isEditing = true + + if (attachmentController.message.value != message) + attachmentController.message.value = message + } + delete -> showDeleteMessageDialog(message) + } + }.show() + } + + private fun showPinMessageDialog( + peerId: Int, + messageId: Int?, + pin: Boolean + ) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle( + if (pin) R.string.confirm_pin_message + else R.string.confirm_unpin_message + ) + .setPositiveButton( + if (pin) R.string.action_pin + else R.string.action_unpin + ) { _, _ -> + viewModel.pinMessage( + peerId = peerId, + messageId = messageId, + pin = pin + ) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun showDeleteMessageDialog(message: VkMessage) { + val binding = DialogMessageDeleteBinding.inflate(layoutInflater, null, false) + + binding.check.setText( + if (message.isOut) R.string.message_delete_for_all + else R.string.message_mark_as_spam + ) + + binding.check.isEnabled = + (conversation.id != UserConfig.userId) && (!message.isOut || message.canEdit()) + + if (conversation.id == UserConfig.userId) binding.check.isChecked = true + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.confirm_delete_message) + .setView(binding.root) + .setPositiveButton(R.string.action_delete) { _, _ -> + attachmentController.message.value = null + + viewModel.deleteMessage( + peerId = conversation.id, + messagesIds = listOf(message.id), + isSpam = if (message.isOut) null else binding.check.isChecked, + deleteForAll = if (!binding.check.isEnabled) null else binding.check.isChecked + ) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun deleteMessages(event: MessagesDelete) { + adapter.removeMessagesByIds(event.messagesIds).let { + it.forEach { index -> adapter.notifyItemRemoved(index) } + } + } + + private fun editMessage(event: MessagesEdit) { + adapter.searchMessageIndex(event.message.id)?.let { index -> + adapter.values[index] = event.message + adapter.notifyItemChanged(index) + } + } + + private inner class AttachmentPanelController { + val isPanelVisible = MutableLiveData(false) + val message = MutableLiveData() + + var isEditing = false + + fun init(): AttachmentPanelController { + message.observe(viewLifecycleOwner) { value -> + if (value != null) { + applyMessage(value) + } else { + clearMessage() } } - dialog.show() + message.value = null + return this + } - } + private fun applyMessage(message: VkMessage) { + showPanel() - private fun onItemLongClick(position: Int): Boolean { + val title = when { + message.isGroup() && message.group.value != null -> message.group.value?.name + message.isUser() && message.user.value != null -> message.user.value?.fullName + else -> null + } + + binding.replyMessageTitle.text = title + binding.replyMessageText.text = message.text ?: "[no_message]" + + if (isEditing) { + binding.message.setText(message.text ?: "[no_message]") + } + } + + private fun clearMessage() { + hidePanel() + + binding.replyMessageTitle.clear() + binding.replyMessageText.clear() + + if (isEditing) { + isEditing = false + binding.message.clear() + } + } + + private fun showPanel(duration: Long = 250) { + if (attachmentController.isPanelVisible.value == false) + attachmentController.isPanelVisible.value = true + + binding.attachmentPanel.animate() + .translationY(0f) + .alpha(1f) + .setDuration(duration) + .withStartAction { binding.attachmentPanel.isVisible = true } + .start() + } + + private fun hidePanel(duration: Long = 250) { + if (attachmentController.isPanelVisible.value == true) + attachmentController.isPanelVisible.value = false + + binding.attachmentPanel.animate() + .alpha(0f) + .translationY(50f) + .setDuration(duration) + .withEndAction { binding.attachmentPanel.isVisible = false } + .start() + } - return true } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt index 2f75959a..a4c07e50 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt @@ -6,33 +6,29 @@ 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.request.MessagesGetHistoryRequest -import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest -import com.meloda.fast.api.model.request.MessagesSendRequest -import com.meloda.fast.api.network.datasource.MessagesDataSource +import com.meloda.fast.api.model.attachments.VkAttachment +import com.meloda.fast.api.network.messages.* import com.meloda.fast.base.viewmodel.BaseViewModel -import com.meloda.fast.base.viewmodel.StartProgressEvent -import com.meloda.fast.base.viewmodel.StopProgressEvent -import com.meloda.fast.base.viewmodel.VKEvent +import com.meloda.fast.base.viewmodel.VkEvent import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class MessagesHistoryViewModel @Inject constructor( - private val dataSource: MessagesDataSource + private val messages: MessagesDataSource ) : BaseViewModel() { fun loadHistory( peerId: Int ) = viewModelScope.launch { makeJob({ - dataSource.getHistory( + messages.getHistory( MessagesGetHistoryRequest( count = 30, peerId = peerId, extended = true, - fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}" + fields = VKConstants.ALL_FIELDS ) ) }, @@ -53,18 +49,18 @@ class MessagesHistoryViewModel @Inject constructor( } } - val messages = hashMapOf() + val hashMessages = hashMapOf() response.items.forEach { baseMessage -> - baseMessage.asVkMessage().let { message -> messages[message.id] = message } + baseMessage.asVkMessage().let { message -> hashMessages[message.id] = message } } - dataSource.storeMessages(messages.values.toList()) + messages.store(hashMessages.values.toList()) val conversations = hashMapOf() response.conversations?.let { baseConversations -> baseConversations.forEach { baseConversation -> baseConversation.asVkConversation( - messages[baseConversation.last_message_id] + hashMessages[baseConversation.last_message_id] ).let { conversation -> conversations[conversation.id] = conversation } } } @@ -75,41 +71,33 @@ class MessagesHistoryViewModel @Inject constructor( profiles = profiles, groups = groups, conversations = conversations, - messages = messages.values.toList() + messages = hashMessages.values.toList() ) ) - }, - onError = { - val throwable = it - throw it - }, - onStart = { sendEvent(StartProgressEvent) }, - onEnd = { sendEvent(StopProgressEvent) }) + }) } fun sendMessage( peerId: Int, message: String? = null, randomId: Int = 0, + replyTo: Int? = null, setId: ((messageId: Int) -> Unit)? = null ) = viewModelScope.launch { makeJob( { - dataSource.send( + messages.send( MessagesSendRequest( peerId = peerId, randomId = randomId, - message = message + message = message, + replyTo = replyTo ) ) }, onAnswer = { val response = it.response ?: return@makeJob setId?.invoke(response) - }, - onError = { - val throwable = it - val i = 0 }) } @@ -118,7 +106,7 @@ class MessagesHistoryViewModel @Inject constructor( important: Boolean ) = viewModelScope.launch { makeJob({ - dataSource.markAsImportant( + messages.markAsImportant( MessagesMarkAsImportantRequest( messagesIds = messagesIds, important = important @@ -133,13 +121,84 @@ class MessagesHistoryViewModel @Inject constructor( important = important ) ) - }, - onError = { - val throwable = it - val i = 0 }) } + fun pinMessage( + peerId: Int, + messageId: Int? = null, + conversationMessageId: Int? = null, + pin: Boolean + ) = viewModelScope.launch { + if (pin) { + makeJob({ + messages.pin( + MessagesPinMessageRequest( + peerId = peerId, + messageId = messageId, + conversationMessageId = conversationMessageId + ) + ) + }, + onAnswer = { + val response = it.response ?: return@makeJob + sendEvent(MessagesPin(response.asVkMessage())) + } + ) + } else { + makeJob({ messages.unpin(MessagesUnPinMessageRequest(peerId = peerId)) }, + onAnswer = { + println("Fast::MessagesHistoryViewModel::unPin::Response::${it.response}") + sendEvent(MessagesUnpin) + } + ) + } + } + + fun deleteMessage( + peerId: Int, + messagesIds: List? = null, + conversationsMessagesIds: List? = null, + isSpam: Boolean? = null, + deleteForAll: Boolean? = null + ) = viewModelScope.launch { + makeJob({ + messages.delete( + MessagesDeleteRequest( + peerId = peerId, + messagesIds = messagesIds, + conversationsMessagesIds = conversationsMessagesIds, + isSpam = isSpam, + deleteForAll = deleteForAll + ) + ) + }, onAnswer = { sendEvent(MessagesDelete(messagesIds = messagesIds ?: listOf())) }) + } + + fun editMessage( + originalMessage: VkMessage, + peerId: Int, + messageId: Int, + message: String? = null, + attachments: List? = null + ) = viewModelScope.launch { + makeJob( + { + messages.edit( + MessagesEditRequest( + peerId = peerId, + messageId = messageId, + message = message, + attachments = attachments + ) + ) + }, + onAnswer = { + originalMessage.text = message + sendEvent(MessagesEdit(originalMessage)) + } + ) + } } data class MessagesLoaded( @@ -148,9 +207,23 @@ data class MessagesLoaded( val messages: List, val profiles: HashMap, val groups: HashMap -) : VKEvent() +) : VkEvent() data class MessagesMarkAsImportant( val messagesIds: List, val important: Boolean -) : VKEvent() \ No newline at end of file +) : VkEvent() + +data class MessagesPin( + val message: VkMessage +) : VkEvent() + +object MessagesUnpin : VkEvent() + +data class MessagesDelete( + val messagesIds: List +) : VkEvent() + +data class MessagesEdit( + val message: VkMessage +) : VkEvent() \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt index 88f3b77d..b5bfc38e 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt @@ -1,6 +1,7 @@ package com.meloda.fast.screens.messages import android.content.Context +import android.graphics.drawable.ColorDrawable import android.util.Log import android.view.View import android.widget.ImageView @@ -8,9 +9,7 @@ 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 @@ -20,7 +19,6 @@ 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.* @@ -30,19 +28,21 @@ import kotlin.math.roundToInt class MessagesPreparator constructor( private val context: Context, + private val root: View? = null, + 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 textContainer: LinearLayoutCompat? = null, private val attachmentContainer: LinearLayoutCompat? = null, private val attachmentSpacer: Space? = null, @@ -65,51 +65,129 @@ class MessagesPreparator constructor( 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) + + private val rootHighlightedColor = + ContextCompat.getColor(context, R.color.n2_100) + + private var photoClickListener: ((url: String) -> Unit)? = null + + fun setPhotoClickListener(unit: ((url: String) -> Unit)?): MessagesPreparator { + this.photoClickListener = unit + return this + } fun prepare() { - val messageUser: VkUser? = if (message.isUser()) { - profiles[message.fromId] - } else null + val messageUser = VkUtils.getMessageUser(message, profiles) + val messageGroup = VkUtils.getMessageGroup(message, groups) - val messageGroup: VkGroup? = if (message.isGroup()) { - groups[message.fromId] - } else null + prepareRootBackground() + prepareTime() + + prepareUnreadIndicator() + + prepareSpacer() + + prepareAttachments() + + prepareAttachmentsSpacer() + + prepareBubbleBackground() + + prepareText() + + prepareAvatar( + messageUser = messageUser, + messageGroup = messageGroup + ) + + if (message.isPeerChat()) { + val prevSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message) + val nextSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(message, nextMessage) + 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: $prevSenderDiff; fiveMinAgo: $fiveMinAgo; " + ) + + title?.isVisible = prevSenderDiff || fiveMinAgo + + avatar?.visibility = + if (nextSenderDiff + || (fiveMinAgo && prevSenderDiff) + || !prevSenderDiff + || nextMessage == null + ) View.VISIBLE else View.INVISIBLE + } 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 + } + } + + private fun prepareRootBackground() { + if (root != null) { + root.background = + if (message.isSelected) ColorDrawable(rootHighlightedColor) + else null + } + } + + private fun prepareTime() { + time?.text = SimpleDateFormat("HH:mm", Locale.getDefault()).format(message.date * 1000L) + } + + private fun prepareUnreadIndicator() { if (unread != null) { unread.isVisible = message.isRead(conversation) } + } - if (bubble != null && time != null) { - bubble.setOnClickListener { time.isVisible = !time.isVisible } - } + private fun prepareSpacer() { + spacer?.isVisible = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message) + } - if (attachmentContainer != null) { + private fun prepareAttachments() { + if (attachmentContainer != null && textContainer != null) { if (message.attachments.isNullOrEmpty()) { attachmentContainer.isVisible = false attachmentContainer.removeAllViews() } else { attachmentContainer.isVisible = true + AttachmentInflater( context = context, container = attachmentContainer, + textContainer = textContainer, message = message, groups = groups, profiles = profiles - ).inflate() + ) + .setPhotoClickListener(photoClickListener) + .inflate() } } + } + private fun prepareAttachmentsSpacer() { + attachmentSpacer?.isVisible = + !message.attachments.isNullOrEmpty() && text?.isVisible == true + } + + private fun prepareBubbleBackground() { 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 @@ -125,82 +203,29 @@ class MessagesPreparator constructor( } } } + } - // 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 - } - + private fun prepareText() { 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) + text.text = VkUtils.prepareMessageText(message.text ?: "") } } + } + private fun prepareAvatar( + messageUser: VkUser? = null, + messageGroup: VkGroup? = null + ) { 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 - } + val avatarUrl = VkUtils.getMessageAvatar(message, messageUser, messageGroup) 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) } } diff --git a/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt new file mode 100644 index 00000000..f3f99443 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt @@ -0,0 +1,48 @@ +package com.meloda.fast.screens.photos + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.fragment.app.viewModels +import com.meloda.fast.base.BaseViewModelFragment +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class PhotoViewFragment : BaseViewModelFragment() { + + override val viewModel: PhotoViewViewModel by viewModels() + +// private val photosList: MutableList = mutableListOf() + + private var photoLink: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + photoLink = requireArguments().getString("photoLink") + +// val list: List<*>? = Gson().fromJson( +// requireArguments().getString("photosList"), +// List::class.java +// ) +// +// list?.forEach { if (it is VkPhoto) photosList.add(it) } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ImageView(requireContext()) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + photoLink?.let { viewModel.loadImageFromUrl(it, requireView() as ImageView) } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt new file mode 100644 index 00000000..b9a2984d --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt @@ -0,0 +1,22 @@ +package com.meloda.fast.screens.photos + +import android.widget.ImageView +import androidx.lifecycle.viewModelScope +import coil.load +import com.meloda.fast.base.viewmodel.BaseViewModel +import kotlinx.coroutines.launch + +class PhotoViewViewModel : BaseViewModel() { + + fun loadImageFromUrl( + url: String, + imageView: ImageView + ) = viewModelScope.launch { + imageView.load(url) + } + + fun saveImageToLocalStorage(url: String) = viewModelScope.launch { + TODO("Not implemented") + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt b/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt index 6a99ef8c..45d9e807 100644 --- a/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt +++ b/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt @@ -1,9 +1,9 @@ package com.meloda.fast.service import android.util.Log -import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest -import com.meloda.fast.api.network.datasource.MessagesDataSource -import com.meloda.fast.api.network.repo.LongPollRepo +import com.meloda.fast.api.network.messages.MessagesGetLongPollServerRequest +import com.meloda.fast.api.network.messages.MessagesDataSource +import com.meloda.fast.api.network.longpoll.LongPollRepo import kotlinx.coroutines.* import javax.inject.Inject import kotlin.coroutines.CoroutineContext diff --git a/app/src/main/kotlin/com/meloda/fast/util/ArrayUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/ArrayUtils.kt deleted file mode 100644 index de96a79d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/ArrayUtils.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.meloda.fast.util - -import java.util.stream.Collectors - -object ArrayUtils { - - @SafeVarargs - fun asString(vararg array: T): String { - if (array.isEmpty()) { - return "" - } - - val builder = StringBuilder(array.size * 12) - builder.append(array[0]) - for (i in 1 until array.size) { - builder.append(',') - builder.append(array[i]) - } - return builder.toString() - } - - fun asString(array: IntArray): String { - if (array.isEmpty()) { - return "" - } - - val builder = StringBuilder(array.size * 12) - builder.append(array[0]) - for (i in 1 until array.size) { - builder.append(',') - builder.append(array[i]) - } - return builder.toString() - } - - fun asString(arrayList: ArrayList): String { - return ArrayList().apply { - arrayList.forEach { add(it.toString()) } - }.stream().collect(Collectors.joining(",")) - } - - fun asString(list: List): String = asString(list.asArrayList()) - - fun cut(arrayList: ArrayList, offset: Int, count: Int): ArrayList { - if (arrayList.isEmpty()) return arrayListOf() - - var lastPosition = offset + count - if (lastPosition > arrayList.size) lastPosition = arrayList.size - - return ArrayList(arrayList.subList(offset, lastPosition)) - } - - fun ByteArray?.isNullOrEmpty() = this == null || this.isEmpty() - - fun List.asArrayList(): ArrayList { - return ArrayList(this) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt deleted file mode 100644 index 9e038147..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.meloda.fast.util - -import android.content.Context -import android.graphics.Color -import androidx.annotation.ColorInt -import com.meloda.fast.R - -object ColorUtils { - - @ColorInt - fun getColorAccent(context: Context): Int { - return AndroidUtils.getThemeAttrColor(context, R.attr.colorAccent) - } - - @ColorInt - fun getColorPrimary(context: Context): Int { - return AndroidUtils.getThemeAttrColor(context, R.attr.colorPrimary) - } - - @JvmOverloads - fun darkenColor(color: Int, darkFactor: Float = 0.75f): Int { - var newColor = color - val hsv = FloatArray(3) - Color.colorToHSV(newColor, hsv) - hsv[2] *= darkFactor - newColor = Color.HSVToColor(hsv) - return newColor - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/util/ImageUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/ImageUtils.kt deleted file mode 100644 index c1d2c056..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/ImageUtils.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.meloda.fast.util - -import android.graphics.Bitmap -import android.graphics.drawable.Drawable -import android.widget.ImageView - -object ImageUtils { - - fun loadImage(image: String, imageView: ImageView, placeholder: Drawable?) { - if (image.isEmpty()) return - -// if (imageView is SimpleDraweeView) { -// imageView.setImageURI(image) -// return -// } -// -// val picasso = Picasso.get() -// .load(image) -// .priority(Picasso.Priority.LOW) - -// if (placeholder != null) picasso.placeholder(placeholder) -// -// picasso.into(imageView) - } - - fun loadImage(image: String?, listener: OnLoadListener?) { - if (image.isNullOrEmpty()) return - -// val picasso = Picasso.get() -// .load(image) -// .priority(Picasso.Priority.LOW) -// -// val target = object : Target { -// override fun onPrepareLoad(placeHolderDrawable: Drawable?) { -// -// } -// -// override fun onBitmapFailed(e: Exception, errorDrawable: Drawable?) { -// listener?.onError(e) -// } -// -// override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) { -// listener?.onLoad(bitmap) -// } -// } - -// picasso.into(target) - } - - - interface OnLoadListener { - fun onLoad(bitmap: Bitmap) - fun onError(e: Exception) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/util/TextUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/TextUtils.kt deleted file mode 100644 index d8524e37..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/TextUtils.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.meloda.fast.util - -object TextUtils { - - fun getFirstLetterFromString(string: String): String { - for (i in string.indices) { - val char = string[i] - - if (char.isLetter()) return char.toString() - } - - return "" - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt index f300bbe2..f934a9e4 100644 --- a/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt +++ b/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt @@ -7,6 +7,8 @@ import java.util.* object TimeUtils { + const val ONE_DAY_IN_SECONDS = 86400 + fun removeTime(date: Date): Long { return Calendar.getInstance().apply { time = date diff --git a/app/src/main/kotlin/com/meloda/fast/util/Utils.kt b/app/src/main/kotlin/com/meloda/fast/util/Utils.kt deleted file mode 100644 index 32497083..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/Utils.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.meloda.fast.util - -import android.content.Context -import com.meloda.fast.util.ArrayUtils.isNullOrEmpty -import com.meloda.fast.R -import com.meloda.fast.io.BytesOutputStream -import java.io.ByteArrayInputStream -import java.io.IOException -import java.io.ObjectInputStream -import java.io.ObjectOutputStream - -object Utils { - - fun getLocalizedThrowable(context: Context, t: Throwable): String { - return context.getString(R.string.error, t.message.toString()) - } - - fun serialize(source: Any?): ByteArray? { - try { - val bos = BytesOutputStream() - val out = ObjectOutputStream(bos) - out.writeObject(source) - out.close() - return bos.byteArray - } catch (e: IOException) { - e.printStackTrace() - } - return null - } - - fun deserialize(source: ByteArray?): Any? { - if (source.isNullOrEmpty()) { - return null - } - - try { - val bis = ByteArrayInputStream(source) - val `in` = ObjectInputStream(bis) - val o = `in`.readObject() - `in`.close() - return o - } catch (e: Exception) { - e.printStackTrace() - } - return null - } - - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/widget/NoItemsView.kt b/app/src/main/kotlin/com/meloda/fast/widget/NoItemsView.kt index 5c1c9729..f6770643 100644 --- a/app/src/main/kotlin/com/meloda/fast/widget/NoItemsView.kt +++ b/app/src/main/kotlin/com/meloda/fast/widget/NoItemsView.kt @@ -10,12 +10,11 @@ import android.widget.TextView import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.appcompat.content.res.AppCompatResources import androidx.core.view.isVisible -import com.meloda.fast.extensions.ContextExtensions.drawable -import com.meloda.fast.extensions.DrawableExtensions.tint -import com.meloda.fast.extensions.FloatExtensions.int import com.meloda.fast.R import com.meloda.fast.util.AndroidUtils +import kotlin.math.roundToInt @Suppress("UNCHECKED_CAST") class NoItemsView @JvmOverloads constructor( @@ -44,7 +43,7 @@ class NoItemsView @JvmOverloads constructor( private fun create() { val a = context.obtainStyledAttributes(attrs, R.styleable.NoItemsView) - minimumWidth = AndroidUtils.px(256).int() + minimumWidth = AndroidUtils.px(256).roundToInt() minimumHeight = minimumWidth orientation = VERTICAL @@ -53,8 +52,8 @@ class NoItemsView @JvmOverloads constructor( noItemsPicture = ImageView(context) val params = imageViewParams - params.height = AndroidUtils.px(64).int() - params.width = AndroidUtils.px(64).int() + params.height = AndroidUtils.px(64).roundToInt() + params.width = AndroidUtils.px(64).roundToInt() noItemsPicture.layoutParams = params @@ -73,10 +72,10 @@ class NoItemsView @JvmOverloads constructor( noItemsTextView = TextView(context) val textParams = textViewParams - textParams.width = AndroidUtils.px(256).int() + textParams.width = AndroidUtils.px(256).roundToInt() if (noItemsDrawable != null) { - textParams.topMargin = AndroidUtils.px(8).int() + textParams.topMargin = AndroidUtils.px(8).roundToInt() } noItemsTextView.layoutParams = textParams @@ -103,7 +102,7 @@ class NoItemsView @JvmOverloads constructor( } fun setNoItemsImage(@DrawableRes resId: Int) { - setNoItemsImage(context.drawable(resId)) + setNoItemsImage(AppCompatResources.getDrawable(context, resId)) } fun setNoItemsImage(drawable: Drawable?) { @@ -111,7 +110,7 @@ class NoItemsView @JvmOverloads constructor( } fun setNoItemsImageTint(@ColorInt color: Int) { - noItemsPicture.drawable.tint(color) + noItemsPicture.drawable?.setTint(color) } fun setNoItemsText(@StringRes resId: Int) { diff --git a/app/src/main/res/drawable-v21/ic_star_border.xml b/app/src/main/res/drawable-v21/ic_star_border.xml index 3fc251d1..f341eb01 100644 --- a/app/src/main/res/drawable-v21/ic_star_border.xml +++ b/app/src/main/res/drawable-v21/ic_star_border.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_chat_attachment_panel_background.xml b/app/src/main/res/drawable/ic_chat_attachment_panel_background.xml new file mode 100644 index 00000000..4e6ca60c --- /dev/null +++ b/app/src/main/res/drawable/ic_chat_attachment_panel_background.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_image_button_circle_background.xml b/app/src/main/res/drawable/ic_image_button_circle_background.xml new file mode 100644 index 00000000..b50a43d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_image_button_circle_background.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_in_background.xml b/app/src/main/res/drawable/ic_message_in_background.xml index b092122a..e2aa9732 100644 --- a/app/src/main/res/drawable/ic_message_in_background.xml +++ b/app/src/main/res/drawable/ic_message_in_background.xml @@ -6,8 +6,8 @@ + android:topRightRadius="30dp" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_out_background.xml b/app/src/main/res/drawable/ic_message_out_background.xml index 71d2120f..d2e74ac8 100644 --- a/app/src/main/res/drawable/ic_message_out_background.xml +++ b/app/src/main/res/drawable/ic_message_out_background.xml @@ -2,6 +2,10 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_round_done_24.xml b/app/src/main/res/drawable/ic_round_done_24.xml new file mode 100644 index 00000000..2231d757 --- /dev/null +++ b/app/src/main/res/drawable/ic_round_done_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_trash_can_outline_24.xml b/app/src/main/res/drawable/ic_trash_can_outline_24.xml new file mode 100644 index 00000000..05862a21 --- /dev/null +++ b/app/src/main/res/drawable/ic_trash_can_outline_24.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/test.png b/app/src/main/res/drawable/test.png deleted file mode 100644 index 6da07119..00000000 Binary files a/app/src/main/res/drawable/test.png and /dev/null differ diff --git a/app/src/main/res/layout/dialog_message_delete.xml b/app/src/main/res/layout/dialog_message_delete.xml new file mode 100644 index 00000000..f5ba3f90 --- /dev/null +++ b/app/src/main/res/layout/dialog_message_delete.xml @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_conversations.xml b/app/src/main/res/layout/fragment_conversations.xml index cf7d291f..0a66795e 100644 --- a/app/src/main/res/layout/fragment_conversations.xml +++ b/app/src/main/res/layout/fragment_conversations.xml @@ -20,18 +20,9 @@ android:elevation="0dp" app:collapsedTitleTextAppearance="@style/CollapsingToolbarCollapsedTitle" app:expandedTitleTextAppearance="@style/CollapsingToolbarTitle" - app:layout_scrollFlags="scroll|enterAlways|snap" + app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" app:title="Messages"> - - + android:paddingStart="0dp" + android:paddingEnd="30dp" + android:paddingBottom="30dp" + app:layout_collapseMode="none"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml index 9378d007..b4f85e10 100644 --- a/app/src/main/res/layout/fragment_login.xml +++ b/app/src/main/res/layout/fragment_login.xml @@ -9,6 +9,14 @@ android:layout_height="match_parent" android:orientation="vertical"> + + - + tools:src="@tools:sample/avatars" /> + android:layout_height="match_parent"> + app:tint="?colorSecondary2" /> + + + + + + + + @@ -182,6 +208,70 @@ + + + + + + + + + + + + + + + + + + + android:hint="@string/message_input_hint" + android:singleLine="true" /> + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_gift.xml b/app/src/main/res/layout/item_message_attachment_gift.xml new file mode 100644 index 00000000..5cc4fe57 --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_gift.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_graffiti.xml b/app/src/main/res/layout/item_message_attachment_graffiti.xml new file mode 100644 index 00000000..5cc4fe57 --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_graffiti.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_link.xml b/app/src/main/res/layout/item_message_attachment_link.xml index fab98353..81b046aa 100644 --- a/app/src/main/res/layout/item_message_attachment_link.xml +++ b/app/src/main/res/layout/item_message_attachment_link.xml @@ -9,12 +9,11 @@ android:orientation="horizontal" android:padding="4dp"> - + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_in.xml b/app/src/main/res/layout/item_message_in.xml index 54f0835f..83f5bc99 100644 --- a/app/src/main/res/layout/item_message_in.xml +++ b/app/src/main/res/layout/item_message_in.xml @@ -51,32 +51,27 @@ android:layout_height="wrap_content" android:background="@drawable/ic_message_in_background" android:backgroundTint="@color/n2_100" + android:minWidth="60dp" android:orientation="vertical" - android:padding="15dp" tools:ignore="UselessParent"> - - - - + android:orientation="vertical"> + + + - + + + android:orientation="vertical" + android:visibility="gone" /> diff --git a/app/src/main/res/layout/item_message_out.xml b/app/src/main/res/layout/item_message_out.xml index 8ac3215d..3b4da87e 100644 --- a/app/src/main/res/layout/item_message_out.xml +++ b/app/src/main/res/layout/item_message_out.xml @@ -30,58 +30,46 @@ android:layout_height="10dp" android:visibility="gone" /> - + android:layout_gravity="end" + android:background="@drawable/ic_message_out_background" + android:orientation="vertical"> - + android:orientation="vertical"> - + + - + - - - - + android:orientation="vertical" + android:visibility="gone" /> diff --git a/app/src/main/res/menu/fragment_conversations.xml b/app/src/main/res/menu/fragment_conversations.xml index aab207c3..e006374c 100644 --- a/app/src/main/res/menu/fragment_conversations.xml +++ b/app/src/main/res/menu/fragment_conversations.xml @@ -2,5 +2,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/messages.xml b/app/src/main/res/navigation/messages.xml index 8db6e5ac..911ca742 100644 --- a/app/src/main/res/navigation/messages.xml +++ b/app/src/main/res/navigation/messages.xml @@ -21,6 +21,17 @@ android:id="@+id/messagesHistoryFragment" android:name="com.meloda.fast.screens.messages.MessagesHistoryFragment" android:label="MessagesHistoryFragment" - tools:layout="@layout/fragment_messages_history" /> + tools:layout="@layout/fragment_messages_history"> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v31/colors.xml b/app/src/main/res/values-v31/colors.xml index ca8ba833..d8bcc1e8 100644 --- a/app/src/main/res/values-v31/colors.xml +++ b/app/src/main/res/values-v31/colors.xml @@ -9,6 +9,7 @@ @android:color/system_accent2_100 @android:color/system_accent2_200 + @android:color/system_accent2_300 @android:color/system_accent2_700 @android:color/system_accent2_1000 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6ed665cd..5a658924 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -39,6 +39,7 @@ #DCE1F7 #C0C6DA + #A4ABBF #414757 #F8D6FC diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml index f1503161..0deb0bf0 100644 --- a/app/src/main/res/values/ic_launcher_background.xml +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -1,4 +1,5 @@ - #4184F5 + + @color/a1_500 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 378c5604..8ea8a128 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,4 +109,43 @@ User post Post Story + Log out + Confirmation + Signing out will delete all data related to this account from this device. Continue? + Yes + No + Reply + Mark as important + Unmark as important + Time: %s + Pin + Unpin + Edit + Delete + + Delete the message? + + Delete for all + + Mark as spam + + Delete + Delete + Delete the conversation? + Sign out + Sign out + Unpin + Pin + Unpin the conversation? + Pin the conversation? + Pin + Unpin + Outgoing call + Incoming call + Ended + Cancelled + Missed + Unknown + Pin the message? + Unpin the message? diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 145b8b2f..517ae724 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -41,5 +41,10 @@ 12dp + \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 98bed167..e5edfa7b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,21 +1,7 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app"s APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn +org.gradle.jvmargs=-Xmx4096M -XX:MaxPermSize=4096m -Dfile.encoding=UTF-8 +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.configureondemand=false android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX android.enableJetifier=true -# Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official \ No newline at end of file