From d1ed98691c7e8ec618f4288490d96557be51da30 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Fri, 17 Sep 2021 16:25:26 +0300 Subject: [PATCH] illegal token checking fixes --- app/build.gradle.kts | 2 + app/src/main/AndroidManifest.xml | 3 +- .../kotlin/com/meloda/fast/api/VKConstants.kt | 2 +- .../kotlin/com/meloda/fast/api/VKException.kt | 7 +- .../kotlin/com/meloda/fast/api/VkUtils.kt | 14 +- .../com/meloda/fast/api/base/ApiError.kt | 4 +- .../meloda/fast/api/model/VkConversation.kt | 4 +- .../com/meloda/fast/api/model/VkGroup.kt | 5 +- .../com/meloda/fast/api/model/VkMessage.kt | 26 +- .../com/meloda/fast/api/model/VkUser.kt | 8 +- .../fast/api/model/attachments/VkSticker.kt | 19 +- .../meloda/fast/api/model/base/BaseVkGroup.kt | 7 +- .../fast/api/model/base/BaseVkLongPoll.kt | 12 + .../meloda/fast/api/model/base/BaseVkUser.kt | 4 +- .../model/base/attachments/BaseVkSticker.kt | 13 +- .../fast/api/model/request/MessagesRequest.kt | 15 +- .../com/meloda/fast/api/network/ErrorCodes.kt | 4 +- .../fast/api/network/ResultCallFactory.kt | 35 +- .../com/meloda/fast/api/network/VKUrls.kt | 2 + .../network/datasource/MessagesDataSource.kt | 4 + .../fast/api/network/repo/LongPollRepo.kt | 18 + .../fast/api/network/repo/MessagesRepo.kt | 5 + .../meloda/fast/base/BaseViewModelFragment.kt | 18 +- .../fast/base/viewmodel/BaseViewModel.kt | 28 +- .../com/meloda/fast/base/viewmodel/Events.kt | 4 + .../com/meloda/fast/database/AppDatabase.kt | 2 +- .../com/meloda/fast/di/NetworkModule.kt | 13 +- .../conversations/ConversationsAdapter.kt | 14 +- .../conversations/ConversationsFragment.kt | 5 +- .../fast/screens/login/LoginFragment.kt | 18 +- .../fast/screens/login/LoginViewModel.kt | 12 +- .../meloda/fast/screens/main/MainFragment.kt | 3 +- .../messages/MessagesHistoryAdapter.kt | 323 +++++++++++++----- .../messages/MessagesHistoryFragment.kt | 22 +- .../messages/MessagesHistoryViewModel.kt | 5 +- .../fast/screens/messages/MessagesManager.kt | 100 ++++++ .../meloda/fast/service/LongPollService.kt | 50 +++ .../com/meloda/fast/widget/CircleImageView.kt | 2 - .../ic_message_in_background_middle.xml | 13 + .../drawable/ic_message_out_background.xml | 2 +- .../ic_message_out_background_middle.xml | 13 + ...c_message_out_background_middle_stroke.xml | 13 + .../ic_message_out_background_stroke.xml | 13 + .../res/layout/fragment_conversations.xml | 3 +- app/src/main/res/layout/item_conversation.xml | 3 +- .../item_message_attachment_photo_out.xml | 20 -- .../item_message_attachment_photos_in.xml | 92 +++++ ...=> item_message_attachment_photos_out.xml} | 15 +- .../item_message_attachment_sticker_in.xml | 41 +++ .../item_message_attachment_sticker_out.xml | 34 ++ app/src/main/res/layout/item_message_in.xml | 46 ++- app/src/main/res/layout/item_message_out.xml | 50 +-- .../main/res/layout/item_message_service.xml | 13 + app/src/main/res/navigation/main.xml | 4 +- app/src/main/res/navigation/nav_graph.xml | 3 +- app/src/main/res/values/colors.xml | 3 + app/src/main/res/values/strings.xml | 6 + 57 files changed, 968 insertions(+), 251 deletions(-) create mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkLongPoll.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/repo/LongPollRepo.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesManager.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt create mode 100644 app/src/main/res/drawable/ic_message_in_background_middle.xml create mode 100644 app/src/main/res/drawable/ic_message_out_background_middle.xml create mode 100644 app/src/main/res/drawable/ic_message_out_background_middle_stroke.xml create mode 100644 app/src/main/res/drawable/ic_message_out_background_stroke.xml delete mode 100644 app/src/main/res/layout/item_message_attachment_photo_out.xml create mode 100644 app/src/main/res/layout/item_message_attachment_photos_in.xml rename app/src/main/res/layout/{item_message_attachment_photo_in.xml => item_message_attachment_photos_out.xml} (53%) create mode 100644 app/src/main/res/layout/item_message_attachment_sticker_in.xml create mode 100644 app/src/main/res/layout/item_message_attachment_sticker_out.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 98bb520a..a22982c8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -79,6 +79,8 @@ dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") + implementation("androidx.work:work-runtime-ktx:2.6.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-alpha02") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c3c3136b..8fc9dd56 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,8 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:testOnly="false" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + tools:replace="android:allowBackup"> ? = null 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 354dafdc..a066d660 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt @@ -13,18 +13,18 @@ import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.attachments.* import com.meloda.fast.api.model.base.BaseVkMessage import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem -import com.meloda.fast.api.network.VKErrors +import com.meloda.fast.api.network.VkErrors object VkUtils { fun isValidationRequired(throwable: Throwable): Boolean { if (throwable !is VKException) return false - return throwable.error == VKErrors.NEED_VALIDATION + return throwable.error == VkErrors.NEED_VALIDATION } fun isCaptchaRequired(throwable: Throwable): Boolean { if (throwable !is VKException) return false - return throwable.error == VKErrors.NEED_CAPTCHA + return throwable.error == VkErrors.NEED_CAPTCHA } fun prepareMessageText(text: String): String { @@ -94,9 +94,7 @@ object VkUtils { } BaseVkAttachmentItem.AttachmentType.STICKER -> { val sticker = baseAttachment.sticker ?: continue - attachments += VkSticker( - link = sticker.images[0].url - ) + attachments += sticker.asVkSticker() } BaseVkAttachmentItem.AttachmentType.GIFT -> { val gift = baseAttachment.gift ?: continue @@ -275,9 +273,9 @@ object VkUtils { else -> return null } ?: return null - val actionMessage = message.actionMessage ?: return null + val actionMessage = message.actionMessage - "$prefix pinned message «$actionMessage»" + "$prefix pinned message ${if (actionMessage == null) "" else "«$actionMessage»"}".trim() } VkMessage.Action.CHAT_UNPIN_MESSAGE -> { val prefix = when { diff --git a/app/src/main/kotlin/com/meloda/fast/api/base/ApiError.kt b/app/src/main/kotlin/com/meloda/fast/api/base/ApiError.kt index 75814dd4..5161ae6d 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/base/ApiError.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/base/ApiError.kt @@ -1,11 +1,11 @@ package com.meloda.fast.api.base import com.google.gson.annotations.SerializedName -import java.io.IOException +import com.meloda.fast.api.VKException data class ApiError( @SerializedName("error_code") val errorCode: Int, @SerializedName("error_msg") override var message: String -) : IOException() +) : VKException(error = message, code = errorCode) 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 55d3ecec..d017aed8 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 @@ -33,8 +33,8 @@ data class VkConversation( fun isUser() = type == "user" fun isGroup() = type == "group" - fun isInUnread() = inRead != lastMessageId - fun isOutUnread() = outRead != lastMessageId + fun isInUnread() = inRead < lastMessageId + fun isOutUnread() = outRead < lastMessageId fun isUnread() = isInUnread() || isOutUnread() diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkGroup.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkGroup.kt index df8aaffd..0e3469a4 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkGroup.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/VkGroup.kt @@ -12,8 +12,9 @@ data class VkGroup( val id: Int, val name: String, val screenName: String, - val photo200: String? -): Parcelable { + val photo200: String?, + val membersCount: Int? +) : Parcelable { override fun toString() = name.trim() 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 fca84ab3..328bc370 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 @@ -42,14 +42,31 @@ data class VkMessage( fun isGroup() = fromId < 0 - fun isRead(conversation: VkConversation) = conversation.outRead < id + fun isRead(conversation: VkConversation) = + if (isOut) conversation.outRead < id + else conversation.inRead < id fun getPreparedAction(): Action? { if (action == null) return null return Action.parse(action) } - fun changeId(id: Int) = VkMessage( + 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, @@ -64,7 +81,10 @@ data class VkMessage( actionMessage = actionMessage, geoType = geoType, important = important - ) + ).also { + it.attachments = attachments + it.forwards = forwards + } enum class Action(val value: String) { CHAT_CREATE("chat_create"), diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkUser.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkUser.kt index c1f5a2e7..93d59131 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkUser.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/VkUser.kt @@ -13,9 +13,13 @@ data class VkUser( val firstName: String, val lastName: String, val online: Boolean, - val photo200: String? + val photo200: String?, + val lastSeen: Int?, + val lastSeenStatus: String? ) : Parcelable { - override fun toString() = "$firstName $lastName".trim() + override fun toString() = fullName + + val fullName get() = "$firstName $lastName".trim() } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkSticker.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkSticker.kt index bc855ba2..c4f18664 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkSticker.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkSticker.kt @@ -1,8 +1,23 @@ package com.meloda.fast.api.model.attachments +import com.meloda.fast.api.model.base.attachments.BaseVkSticker +import com.meloda.fast.api.model.base.attachments.StickerSize import kotlinx.parcelize.Parcelize @Parcelize data class VkSticker( - val link: String -) : VkAttachment() + val id: Int, + val productId: Int, + val images: List, + val backgroundImages: List +) : VkAttachment() { + + fun urlForSize(@StickerSize size: Int): String? { + for (image in images) { + if (image.width == size) return image.url + } + + return null + } + +} diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkGroup.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkGroup.kt index 0bf754c3..fbbcc684 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkGroup.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkGroup.kt @@ -25,14 +25,17 @@ data class BaseVkGroup( @SerializedName("photo_100") val photo100: String?, @SerializedName("photo_200") - val photo200: String? + val photo200: String?, + @SerializedName("members_count") + val membersCount: Int? ) : Parcelable { fun asVkGroup() = VkGroup( id = -id, name = name, screenName = screenName, - photo200 = photo200 + photo200 = photo200, + membersCount = membersCount ) } diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkLongPoll.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkLongPoll.kt new file mode 100644 index 00000000..cf5d0814 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkLongPoll.kt @@ -0,0 +1,12 @@ +package com.meloda.fast.api.model.base + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class BaseVkLongPoll( + val server: String, + val key: String, + val ts: Int, + val pts: Int +) : Parcelable \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkUser.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkUser.kt index bc7ceac9..0f16b727 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkUser.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkUser.kt @@ -52,7 +52,9 @@ data class BaseVkUser( firstName = firstName, lastName = lastName, online = online == 1, - photo200 = photo200 + photo200 = photo200, + lastSeen = onlineInfo?.lastSeen, + lastSeenStatus = onlineInfo?.status ) } diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkSticker.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkSticker.kt index 14dfb4a0..c162e57f 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkSticker.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkSticker.kt @@ -1,7 +1,9 @@ package com.meloda.fast.api.model.base.attachments import android.os.Parcelable +import androidx.annotation.IntDef import com.google.gson.annotations.SerializedName +import com.meloda.fast.api.model.attachments.VkSticker import kotlinx.parcelize.Parcelize @Parcelize @@ -18,6 +20,13 @@ data class BaseVkSticker( val animations: List? ) : Parcelable { + fun asVkSticker() = VkSticker( + id = stickerId, + productId = productId, + images = images, + backgroundImages = imagesWithBackground + ) + @Parcelize data class Image( val width: Int, @@ -31,5 +40,7 @@ data class BaseVkSticker( val url: String ) : Parcelable +} -} \ No newline at end of file +@IntDef(64, 128, 256, 352) +annotation class StickerSize 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 index 3bc740b1..fda2f97f 100644 --- 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 @@ -1,7 +1,6 @@ package com.meloda.fast.api.model.request import android.os.Parcelable -import com.google.gson.annotations.SerializedName import kotlinx.parcelize.Parcelize @Parcelize @@ -59,7 +58,6 @@ data class MessagesSendRequest( @Parcelize data class MessagesMarkAsImportantRequest( - @SerializedName("message_ids") val messagesIds: List, val important: Boolean ) : Parcelable { @@ -70,4 +68,17 @@ data class MessagesMarkAsImportantRequest( "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/network/ErrorCodes.kt b/app/src/main/kotlin/com/meloda/fast/api/network/ErrorCodes.kt index 12c13ecd..fcf53a0d 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/ErrorCodes.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/ErrorCodes.kt @@ -1,6 +1,6 @@ package com.meloda.fast.api.network -object ErrorCodes { +object VkErrorCodes { const val UNKNOWN_ERROR = 1 const val APP_DISABLED = 2 const val UNKNOWN_METHOD = 3 @@ -41,7 +41,7 @@ object ErrorCodes { const val ACCESS_TO_DOC_DENIED = 1153 } -object VKErrors { +object VkErrors { const val UNKNOWN = "unknown_error" const val NEED_VALIDATION = "need_validation" 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 9cf21bd5..e6afbc0b 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,10 +1,9 @@ package com.meloda.fast.api.network -import com.meloda.fast.api.VKException +import com.meloda.fast.api.base.ApiResponse import okhttp3.Request import okio.IOException import okio.Timeout -import org.json.JSONObject import retrofit2.* import java.lang.reflect.ParameterizedType import java.lang.reflect.Type @@ -76,11 +75,16 @@ internal class ResultCall(proxy: Call) : CallDelegate>(proxy) ) : Callback { override fun onResponse(call: Call, response: Response) { - val result: Answer = if (response.isSuccessful) - Answer.Success(response.body() as T) - else Answer.Error(IOException(response.errorBody()?.string() ?: "")) - - if (result is Answer.Error) if (checkErrors(call, result)) return + val result: Answer = + if (response.isSuccessful) { + val baseBody = response.body() + if (baseBody !is ApiResponse<*>) Answer.Success(baseBody as T) + else { + val body = baseBody as ApiResponse<*> + if (body.error != null) Answer.Error(body.error) + else Answer.Success(body as T) + } + } else Answer.Error(IOException(response.errorBody()?.string() ?: "")) callback.onResponse(proxy, Response.success(result)) } @@ -91,23 +95,6 @@ internal class ResultCall(proxy: Call) : CallDelegate>(proxy) Response.success(Answer.Error(throwable = error)) ) } - - private fun checkErrors(call: Call, result: Answer.Error): Boolean { - val json = JSONObject(result.throwable.message ?: "{}") - - return if (json.has("error")) { - val error = json.optString("error", "") - val description = json.optString("error_description", "") - - val exception = VKException( - error = error, - description = description, - ).also { it.json = json } - - onFailure(call, exception) - true - } else false - } } override fun timeout(): Timeout { 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 index cde80070..d0476e13 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 @@ -22,6 +22,8 @@ object VKUrls { const val GetHistory = "$API/messages.getHistory" const val Send = "$API/messages.send" const val MarkAsImportant = "$API/messages.markAsImportant" + const val GetLongPollServer = "$API/messages.getLongPollServer" + const val GetLongPollHistory = "$API/messages.getLongPollHistory" } 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/datasource/MessagesDataSource.kt index 4f19fc53..1a871c57 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.network.datasource 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 @@ -21,4 +22,7 @@ class MessagesDataSource @Inject constructor( suspend fun markAsImportant(params: MessagesMarkAsImportantRequest) = repo.markAsImportant(params.map) + suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) = + repo.getLongPollServer(params.map) + } \ 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/repo/LongPollRepo.kt new file mode 100644 index 00000000..793f0940 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/repo/LongPollRepo.kt @@ -0,0 +1,18 @@ +package com.meloda.fast.api.network.repo + +import com.meloda.fast.api.base.ApiResponse +import com.meloda.fast.api.network.Answer +import org.json.JSONObject +import retrofit2.http.GET +import retrofit2.http.Path +import retrofit2.http.QueryMap + +interface LongPollRepo { + + @GET("https://{serverUrl}") + suspend fun getResponse( + @Path("serverUrl") serverUrl: String, + @QueryMap 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 index 1476113a..831d4c49 100644 --- 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 @@ -1,6 +1,7 @@ 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 @@ -22,4 +23,8 @@ interface MessagesRepo { @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/base/BaseViewModelFragment.kt b/app/src/main/kotlin/com/meloda/fast/base/BaseViewModelFragment.kt index a9f7eb55..cb21a3e1 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/BaseViewModelFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/BaseViewModelFragment.kt @@ -1,10 +1,16 @@ package com.meloda.fast.base +import android.content.Intent import android.os.Bundle import android.view.View +import android.widget.Toast import androidx.annotation.LayoutRes import androidx.lifecycle.lifecycleScope +import com.meloda.fast.R +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 kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach @@ -24,6 +30,16 @@ 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 + ).show() + + UserConfig.clear() + requireActivity().finishAffinity() + requireActivity().startActivity(Intent(requireContext(), MainActivity::class.java)) + } + } } \ 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 1a85538a..b47feff0 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 @@ -4,9 +4,10 @@ import android.util.Log import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.meloda.fast.api.VKException +import com.meloda.fast.api.base.ApiError import com.meloda.fast.api.network.Answer -import com.meloda.fast.api.network.VKErrors -import com.meloda.fast.util.Utils +import com.meloda.fast.api.network.VkErrorCodes +import com.meloda.fast.api.network.VkErrors import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @@ -26,23 +27,32 @@ abstract class BaseViewModel : ViewModel() { onStart?.invoke() when (val response = job()) { is Answer.Success -> onAnswer(response.data) - is Answer.Error -> onError?.invoke(response.throwable) + is Answer.Error -> { + checkErrors(response.throwable) + onError?.invoke(response.throwable) + } } }.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } } protected suspend fun sendEvent(event: T) = tasksEventChannel.send(event) - protected suspend fun checkErrors(throwable: Throwable) { - // TODO: 8/31/2021 check illegal token - if (throwable is VKException) { + private suspend fun checkErrors(throwable: Throwable) { + if (throwable is ApiError) { + when (throwable.errorCode) { + VkErrorCodes.USER_AUTHORIZATION_FAILED -> { + sendEvent(IllegalTokenEvent) + return + } + } + } else if (throwable is VKException) { when (throwable.error) { - VKErrors.NEED_CAPTCHA -> { + VkErrors.NEED_CAPTCHA -> { throwable.captcha = (throwable.json?.optString("captcha_sid") ?: "") to (throwable.json?.optString("captcha_img") ?: "") return } - VKErrors.NEED_VALIDATION -> { + VkErrors.NEED_VALIDATION -> { throwable.validationSid = throwable.json?.optString("validation_sid") return } @@ -51,7 +61,7 @@ abstract class BaseViewModel : ViewModel() { } } - tasksEventChannel.send(ShowDialogInfoEvent(null, Log.getStackTraceString(throwable))) + sendEvent(ShowDialogInfoEvent(null, Log.getStackTraceString(throwable))) } } \ No newline at end of file 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 f06d859d..a24c4e43 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 @@ -7,6 +7,10 @@ data class ShowDialogInfoEvent( val negativeBtn: String? = null ) : VKEvent() +data class ErrorEvent(val errorText: String) : VKEvent() + +object IllegalTokenEvent : VKEvent() + object StartProgressEvent : VKEvent() object StopProgressEvent : VKEvent() 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 fdb4ddd3..86616292 100644 --- a/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt +++ b/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt @@ -18,7 +18,7 @@ import com.meloda.fast.database.dao.UsersDao VkUser::class, VkGroup::class ], - version = 16, + version = 18, exportSchema = false ) abstract class AppDatabase : RoomDatabase() { 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 4825ad45..3ebaa503 100644 --- a/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt +++ b/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt @@ -2,16 +2,13 @@ package com.meloda.fast.di 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.AuthInterceptor -import com.meloda.fast.api.network.ResultCallFactory -import com.meloda.fast.api.network.repo.AuthRepo -import com.meloda.fast.api.network.repo.ConversationsRepo -import com.meloda.fast.api.network.repo.MessagesRepo -import com.meloda.fast.api.network.repo.UsersRepo +import com.meloda.fast.api.network.repo.* import com.meloda.fast.database.dao.ConversationsDao import com.meloda.fast.database.dao.MessagesDao import com.meloda.fast.database.dao.UsersDao @@ -81,6 +78,10 @@ object NetworkModule { fun provideMessagesRepo(retrofit: Retrofit): MessagesRepo = retrofit.create(MessagesRepo::class.java) + @Provides + fun provideLongPollRepo(retrofit: Retrofit): LongPollRepo = + retrofit.create(LongPollRepo::class.java) + @Provides @Singleton fun provideAuthDataSource( 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 726257c0..b52c8069 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 @@ -88,9 +88,10 @@ class ConversationsAdapter constructor( } binding.avatar.isVisible = avatar != null - binding.avatarPlaceholder.isVisible = avatar == null if (avatar == null) { + binding.avatarPlaceholder.isVisible = true + if (conversation.ownerId == VKConstants.FAST_GROUP_ID) { binding.placeholderBack.setImageDrawable( ColorDrawable( @@ -114,7 +115,13 @@ class ConversationsAdapter constructor( binding.avatar.setImageDrawable(null) } } else { - binding.avatar.load(avatar) { crossfade(200) } + binding.avatar.load(avatar) { + crossfade(200) + target { + binding.avatarPlaceholder.isVisible = false + binding.avatar.setImageDrawable(it) + } + } } binding.online.isVisible = chatUser?.online == true @@ -155,7 +162,8 @@ class ConversationsAdapter constructor( message = message ) else null - val messageText = (if (actionMessage != null || + val messageText = (if ( + actionMessage != null || forwardsMessage != null || attachmentText != null ) "" 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 df309ada..4c97e5b5 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 @@ -45,6 +45,7 @@ class ConversationsFragment : } private var isPaused = false + private var isExpanded = true override fun onPause() { super.onPause() @@ -63,7 +64,9 @@ class ConversationsFragment : it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } } } - binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> + binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset -> + if (isPaused) return@OnOffsetChangedListener + if (verticalOffset <= -100) { binding.avatarContainer.alpha = 0f return@OnOffsetChangedListener 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 f1f5dc25..cc2be0ae 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 @@ -6,6 +6,7 @@ import android.view.KeyEvent import android.view.View import android.view.inputmethod.EditorInfo import android.viewbinding.library.fragment.viewBinding +import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible import androidx.core.widget.addTextChangedListener @@ -48,6 +49,11 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo private var captchaInputLayout: TextInputLayout? = null private var validationInputLayout: TextInputLayout? = null + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel.unknownErrorDefaultText = getString(R.string.unknown_error_occurred) + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -156,7 +162,6 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo ) } - // TODO: 7/27/2021 extract strings to resources private fun validateInputData( loginString: String?, passwordString: String?, @@ -167,22 +172,22 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo if (loginString?.isEmpty() == true) { isValidated = false - setError("Input login", binding.loginLayout) + setError(getString(R.string.input_login_hint), binding.loginLayout) } if (passwordString?.isEmpty() == true) { isValidated = false - setError("Input password", binding.passwordLayout) + setError(getString(R.string.input_password_hint), binding.passwordLayout) } if (captchaCode?.isEmpty() == true && captchaInputLayout != null) { isValidated = false - setError("Input code", captchaInputLayout!!) + setError(getString(R.string.input_code_hint), captchaInputLayout!!) } if (validationCode?.isEmpty() == true && validationInputLayout != null) { isValidated = false - setError("Input code", validationInputLayout!!) + setError(getString(R.string.input_code_hint), validationInputLayout!!) } return isValidated @@ -281,9 +286,8 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo validationBinding.cancel.setOnClickListener { dialog.dismiss() } } - // TODO: 8/31/2021 show snackbar private fun showValidationRequired() { - + Toast.makeText(requireContext(), R.string.validation_required, Toast.LENGTH_LONG).show() } private fun showErrorSnackbar(errorDescription: String) { 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 ffed3866..73e16c16 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 @@ -6,12 +6,9 @@ import com.meloda.fast.api.UserConfig import com.meloda.fast.api.VKConstants import com.meloda.fast.api.VKException import com.meloda.fast.api.VkUtils -import com.meloda.fast.api.network.datasource.AuthDataSource import com.meloda.fast.api.model.request.RequestAuthDirect -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.api.network.datasource.AuthDataSource +import com.meloda.fast.base.viewmodel.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @@ -21,6 +18,8 @@ class LoginViewModel @Inject constructor( private val dataSource: AuthDataSource ) : BaseViewModel() { + lateinit var unknownErrorDefaultText: String + fun login( login: String, password: String, @@ -45,8 +44,8 @@ class LoginViewModel @Inject constructor( ) }, onAnswer = { - // TODO: 8/31/2021 do something if (it.userId == null || it.accessToken == null) { + sendEvent(ErrorEvent(unknownErrorDefaultText)) return@makeJob } @@ -56,7 +55,6 @@ class LoginViewModel @Inject constructor( sendEvent(SuccessAuth(haveAuthorized = true)) }, onError = { - checkErrors(it) if (it !is VKException) return@makeJob twoFaCode?.let { sendEvent(CodeSent) } 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 90f97be7..58f5fcf9 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 @@ -21,9 +21,8 @@ class MainFragment : BaseViewModelFragment(R.layout.fragment_main override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - if (savedInstanceState == null) setupBottomBar() - if (!UserConfig.isLoggedIn()) findNavController().navigate(R.id.toLogin) + else if (savedInstanceState == null) setupBottomBar() } private fun setupBottomBar() { 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 542d31dc..5f93cddd 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 @@ -1,10 +1,14 @@ package com.meloda.fast.screens.messages import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.Gravity import android.view.View import android.view.ViewGroup -import android.widget.FrameLayout +import android.widget.Toast import androidx.appcompat.widget.LinearLayoutCompat +import androidx.core.content.ContextCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import coil.load @@ -15,9 +19,9 @@ import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.attachments.VkPhoto +import com.meloda.fast.api.model.attachments.VkSticker import com.meloda.fast.base.adapter.BaseAdapter import com.meloda.fast.base.adapter.BaseHolder -import com.meloda.fast.common.AppGlobal import com.meloda.fast.databinding.* import com.meloda.fast.util.AndroidUtils import kotlin.math.roundToInt @@ -31,30 +35,31 @@ class MessagesHistoryAdapter constructor( ) : BaseAdapter(context, values, COMPARATOR) { override fun getItemViewType(position: Int): Int { - var viewType: Int = when { - isPositionHeader(position) -> HEADER - isPositionFooter(position) -> FOOTER - else -> -1 + when { + isPositionHeader(position) -> return HEADER + isPositionFooter(position) -> return FOOTER } - if (viewType == -1) { - getItem(position).let { - if (it.action != null) viewType = SERVICE + getItem(position).let { message -> + if (message.action != null) return SERVICE - val attachments = it.attachments ?: return@let - if (attachments.isEmpty()) return@let + if (!message.attachments.isNullOrEmpty()) { + val attachments = message.attachments ?: return@let if (VkUtils.isAttachmentsHaveOneType(attachments) && attachments[0] is VkPhoto - ) { - return if (it.isOut) ATTACHMENT_PHOTOS_OUT else ATTACHMENT_PHOTOS_IN - } + ) return if (message.isOut) ATTACHMENT_PHOTOS_OUT + else ATTACHMENT_PHOTOS_IN - if (it.isOut) viewType = OUTGOING - if (!it.isOut) viewType = INCOMING + + if (attachments[0] is VkSticker) return if (message.isOut) ATTACHMENT_STICKER_OUT + else ATTACHMENT_STICKER_IN } + + if (message.isOut) return OUTGOING + if (!message.isOut) return INCOMING } - return viewType + return -1 } private fun isPositionHeader(position: Int) = position == 0 @@ -67,11 +72,17 @@ class MessagesHistoryAdapter constructor( SERVICE -> ServiceMessage( ItemMessageServiceBinding.inflate(inflater, parent, false) ) + ATTACHMENT_STICKER_IN -> AttachmentStickerIncoming( + ItemMessageAttachmentStickerInBinding.inflate(inflater, parent, false) + ) + ATTACHMENT_STICKER_OUT -> AttachmentStickerOutgoing( + ItemMessageAttachmentStickerOutBinding.inflate(inflater, parent, false) + ) ATTACHMENT_PHOTOS_IN -> AttachmentPhotosIncoming( - ItemMessageAttachmentPhotoInBinding.inflate(inflater, parent, false) + ItemMessageAttachmentPhotosInBinding.inflate(inflater, parent, false) ) ATTACHMENT_PHOTOS_OUT -> AttachmentPhotosOutgoing( - ItemMessageAttachmentPhotoOutBinding.inflate(inflater, parent, false) + ItemMessageAttachmentPhotosOutBinding.inflate(inflater, parent, false) ) OUTGOING -> OutgoingMessage( ItemMessageOutBinding.inflate(inflater, parent, false) @@ -79,7 +90,7 @@ class MessagesHistoryAdapter constructor( INCOMING -> IncomingMessage( ItemMessageInBinding.inflate(inflater, parent, false) ) - else -> Holder() + else -> throw IllegalStateException("Wrong viewType: $viewType") } } @@ -107,56 +118,126 @@ class MessagesHistoryAdapter constructor( inner class Footer(v: View) : Holder(v) - inner class AttachmentPhotosIncoming( - private val binding: ItemMessageAttachmentPhotoInBinding + inner class IncomingMessage( + private val binding: ItemMessageInBinding ) : Holder(binding.root) { + private val backgroundNormal = + ContextCompat.getDrawable(context, R.drawable.ic_message_in_background) + private val backgroundMiddle = + ContextCompat.getDrawable(context, R.drawable.ic_message_in_background_middle) + init { - binding.photo.shapeAppearanceModel = binding.photo.shapeAppearanceModel.withCornerSize { - AndroidUtils.px(12) - } + MessagesManager.setRootMaxWidth(binding.bubble) } override fun bind(position: Int) { val message = getItem(position) - val photo = message.attachments?.get(0) as? VkPhoto ?: return + val prevMessage = getOrNull(position - 1) + val nextMessage = getOrNull(position + 1) - val size = photo.sizeOfType('m') ?: return + binding.unread.isVisible = message.isRead(conversation) - binding.photo.layoutParams = FrameLayout.LayoutParams( - AndroidUtils.px(size.width).roundToInt(), - AndroidUtils.px(size.height).roundToInt() + binding.bubble.background = + if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormal + else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddle + else backgroundNormal + + if (!message.isPeerChat()) { + binding.title.isVisible = false + binding.avatar.isVisible = false + + binding.spacer.isVisible = + !(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) + } else { + binding.title.isVisible = + if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat() + else message.date - prevMessage.date >= 60 + + binding.spacer.isVisible = binding.title.isVisible + + binding.avatar.visibility = + if (nextMessage == null || nextMessage.fromId != message.fromId) if (message.isPeerChat()) View.VISIBLE else View.GONE + else if (nextMessage.date - message.date >= 60) View.VISIBLE + else View.INVISIBLE + } + + val messageUser: VkUser? = if (message.isUser()) { + profiles[message.fromId] + } else null + + val messageGroup: VkGroup? = if (message.isGroup()) { + groups[message.fromId] + } else null + + MessagesManager.loadMessageAvatar( + message = message, + messageUser = messageUser, + messageGroup = messageGroup, + imageView = binding.avatar ) - binding.photo.load(size.url) - } + val title = when { + message.isUser() && messageUser != null -> messageUser.firstName + message.isGroup() && messageGroup != null -> messageGroup.name + else -> null + } + binding.title.text = title + binding.title.measure(0, 0) + + if (binding.title.isVisible) { + binding.bubble.minimumWidth = binding.title.measuredWidth + 60 + } else { + binding.bubble.minimumWidth = 0 + } + + MessagesManager.setMessageText( + message = message, + textView = binding.text + ) + } } - inner class AttachmentPhotosOutgoing( - private val binding: ItemMessageAttachmentPhotoOutBinding + inner class OutgoingMessage( + private val binding: ItemMessageOutBinding ) : Holder(binding.root) { + private val backgroundNormal = + ContextCompat.getDrawable(context, R.drawable.ic_message_out_background) + private val backgroundMiddle = + ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle) + private val backgroundStroke = + ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke) + private val backgroundMiddleStroke = + ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke) + init { - binding.photo.shapeAppearanceModel = binding.photo.shapeAppearanceModel.withCornerSize { - AndroidUtils.px(12) - } + MessagesManager.setRootMaxWidth(binding.bubble) } override fun bind(position: Int) { val message = getItem(position) - val photo = message.attachments?.get(0) as? VkPhoto ?: return + val prevMessage = getOrNull(position - 1) - val size = photo.sizeOfType('m') ?: return + binding.text.text = message.text ?: "[no_message]" - binding.photo.layoutParams = LinearLayoutCompat.LayoutParams( - AndroidUtils.px(size.width).roundToInt(), - AndroidUtils.px(size.height).roundToInt() - ) + binding.unread.isVisible = message.isRead(conversation) - binding.photo.load(size.url) + binding.spacer.isVisible = + !(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) + + binding.bubble.background = + if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormal + else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddle + else backgroundNormal + + binding.bubbleStroke.background = + if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundStroke + else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleStroke + else backgroundStroke } } @@ -167,6 +248,12 @@ class MessagesHistoryAdapter constructor( private val youPrefix = context.getString(R.string.you_message_prefix) + init { + binding.photo.shapeAppearanceModel.run { + withCornerSize { AndroidUtils.px(4) } + } + } + override fun bind(position: Int) { val message = getItem(position) @@ -188,49 +275,112 @@ class MessagesHistoryAdapter constructor( messageUser = messageUser, messageGroup = messageGroup ) + + val attachments = message.attachments ?: return + attachments[0].let { attachment -> + if (attachment !is VkPhoto) return@let + + binding.photo.isVisible = true + + val size = attachment.sizeOfType('m') ?: return@let + + binding.photo.layoutParams = LinearLayoutCompat.LayoutParams( + size.width, + size.height + ) + + binding.photo.load(size.url) { + crossfade(150) + fallback(ColorDrawable(Color.LTGRAY)) + } + } } } - inner class OutgoingMessage( - private val binding: ItemMessageOutBinding + inner class AttachmentPhotosIncoming( + private val binding: ItemMessageAttachmentPhotosInBinding ) : Holder(binding.root) { - init { - binding.bubble.maxWidth = (AppGlobal.screenWidth * 0.75).roundToInt() - } - override fun bind(position: Int) { val message = getItem(position) - binding.text.text = message.text ?: "[no_message]" - - binding.unread.isVisible = message.isRead(conversation) + MessagesManager.loadPhotos( + context = context, + message = message, + binding.photosContainer + ) } - } - inner class IncomingMessage( - private val binding: ItemMessageInBinding + inner class AttachmentPhotosOutgoing( + private val binding: ItemMessageAttachmentPhotosOutBinding ) : Holder(binding.root) { - init { - binding.bubble.maxWidth = (AppGlobal.screenWidth * 0.7).roundToInt() - } - override fun bind(position: Int) { val message = getItem(position) + MessagesManager.loadPhotos( + context = context, + message = message, + photosContainer = binding.photosContainer + ) + } + } + + inner class AttachmentStickerOutgoing( + private val binding: ItemMessageAttachmentStickerOutBinding + ) : Holder(binding.root) { + + override fun bind(position: Int) { + val message = getItem(position) val prevMessage = getOrNull(position - 1) val nextMessage = getOrNull(position + 1) - binding.title.isVisible = - if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat() - else message.date - prevMessage.date >= 60 + if (!message.isPeerChat()) { + binding.spacer.isVisible = + !(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) + } else { + binding.spacer.isVisible = + if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat() + else message.date - prevMessage.date >= 60 + } - binding.avatar.visibility = - if (nextMessage == null || nextMessage.fromId != message.fromId) if (message.isPeerChat()) View.VISIBLE else View.GONE - else if (nextMessage.date - message.date >= 60) View.VISIBLE - else View.INVISIBLE + val sticker = message.attachments?.get(0) as? VkSticker ?: return + val url = sticker.urlForSize(352)!! + + binding.photo.layoutParams.also { + it.width = 352 + it.height = 352 + } + + binding.photo.load(url) { crossfade(150) } + } + } + + inner class AttachmentStickerIncoming( + private val binding: ItemMessageAttachmentStickerInBinding + ) : Holder(binding.root) { + + override fun bind(position: Int) { + val message = getItem(position) + val prevMessage = getOrNull(position - 1) + val nextMessage = getOrNull(position + 1) + + if (!message.isPeerChat()) { + binding.avatar.isVisible = false + + binding.spacer.isVisible = + !(prevMessage != null && prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) + } else { + binding.spacer.isVisible = + if (prevMessage == null || prevMessage.fromId != message.fromId) message.isPeerChat() + else message.date - prevMessage.date >= 60 + + binding.avatar.visibility = + if (nextMessage == null || nextMessage.fromId != message.fromId) if (message.isPeerChat()) View.VISIBLE else View.GONE + else if (nextMessage.date - message.date >= 60) View.VISIBLE + else View.INVISIBLE + } val messageUser: VkUser? = if (message.isUser()) { profiles[message.fromId] @@ -246,24 +396,34 @@ class MessagesHistoryAdapter constructor( else -> null } + binding.avatar.load(avatar) { crossfade(100) } + val title = when { - message.isUser() && messageUser != null -> messageUser.firstName + message.isUser() && messageUser != null -> messageUser.fullName message.isGroup() && messageGroup != null -> messageGroup.name else -> null } - binding.avatar.load(avatar) { crossfade(100) } - - binding.text.text = message.text ?: "[no_message]" - - binding.title.text = title - binding.title.measure(0, 0) - - if (binding.title.isVisible) { - binding.bubble.minimumWidth = binding.title.measuredWidth + 60 - } else { - binding.bubble.minimumWidth = 0 + binding.avatar.setOnLongClickListener { + Toast.makeText(context, title, Toast.LENGTH_SHORT).apply { + setGravity( + Gravity.START or Gravity.BOTTOM, + 0, + -50 + ) + }.show() + true } + + val sticker = message.attachments?.get(0) as? VkSticker ?: return + val url = sticker.urlForSize(352)!! + + binding.photo.layoutParams.also { + it.width = 352 + it.height = 352 + } + + binding.photo.load(url) { crossfade(150) } } } @@ -281,8 +441,11 @@ class MessagesHistoryAdapter constructor( private const val INCOMING = 3 private const val OUTGOING = 4 + private const val ATTACHMENT_PHOTOS_IN = 101 - private const val ATTACHMENT_PHOTOS_OUT = 1011 + private const val ATTACHMENT_PHOTOS_OUT = 102 + private const val ATTACHMENT_STICKER_IN = 111 + private const val ATTACHMENT_STICKER_OUT = 112 private val COMPARATOR = object : DiffUtil.ItemCallback() { override fun areItemsTheSame( 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 679578a4..6809ee16 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 @@ -80,8 +80,15 @@ class MessagesHistoryFragment : val status = when { conversation.isChat() -> "${conversation.membersCount} members" - conversation.isUser() -> if (user?.online == true) "Online" else "Last seen at [...]" - conversation.isGroup() -> "[Group status]" + conversation.isUser() -> when { + // TODO: 9/15/2021 user normal time + user?.online == true -> "Online" + user?.lastSeen != null -> "Last seen at ${ + SimpleDateFormat("HH:mm", Locale.getDefault()).format(user?.lastSeen!! * 1000L) + }" + else -> if (user?.lastSeenStatus != null) "Last seen ${user?.lastSeenStatus!!}" else "Last seen recently" + } + conversation.isGroup() -> if (group?.membersCount != null) "${group?.membersCount} members" else "Group" else -> null } @@ -162,7 +169,6 @@ class MessagesHistoryFragment : } action.observe(viewLifecycleOwner) { - binding.action.animate() .scaleX(1.25f) .scaleY(1.25f) @@ -216,18 +222,19 @@ class MessagesHistoryFragment : peerId = conversation.id, message = messageText, randomId = 0 - ) { message = message.changeId(it) } + ) { message = message.copyMessage(id = it) } } } override fun onEvent(event: VKEvent) { + super.onEvent(event) + when (event) { is MessagesMarkAsImportant -> markMessagesAsImportant(event) is MessagesLoaded -> refreshMessages(event) is StartProgressEvent -> onProgressStarted() is StopProgressEvent -> onProgressStopped() } - super.onEvent(event) } private fun onProgressStarted() { @@ -276,7 +283,9 @@ class MessagesHistoryFragment : val message = adapter.values[i] if (event.messagesIds.contains(message.id)) { if (!changed) changed = true - adapter.values[i] = message.copy(important = event.important) + adapter.values[i] = message.copyMessage( + important = event.important + ) } } @@ -303,6 +312,7 @@ class MessagesHistoryFragment : private fun onItemClick(position: Int) { val message = adapter.values[position] + if (message.action != null) return val important = if (message.important) "Unmark as important" else "Mark as important" 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 776877a8..9ff3e05b 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 @@ -1,6 +1,7 @@ package com.meloda.fast.screens.messages import androidx.lifecycle.viewModelScope +import com.meloda.fast.api.VKConstants import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkMessage @@ -28,10 +29,10 @@ class MessagesHistoryViewModel @Inject constructor( makeJob({ dataSource.getHistory( MessagesGetHistoryRequest( - count = 90, + count = 30, peerId = peerId, extended = true, - fields = "photo_200,sex" + fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}" ) ) }, diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesManager.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesManager.kt new file mode 100644 index 00000000..3351e794 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesManager.kt @@ -0,0 +1,100 @@ +package com.meloda.fast.screens.messages + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.Space +import android.widget.TextView +import androidx.appcompat.widget.LinearLayoutCompat +import androidx.core.view.isNotEmpty +import coil.load +import com.google.android.material.imageview.ShapeableImageView +import com.meloda.fast.api.model.VkGroup +import com.meloda.fast.api.model.VkMessage +import com.meloda.fast.api.model.VkUser +import com.meloda.fast.api.model.attachments.VkPhoto +import com.meloda.fast.common.AppGlobal +import com.meloda.fast.util.AndroidUtils +import com.meloda.fast.widget.BoundedFrameLayout +import com.meloda.fast.widget.BoundedLinearLayout +import kotlin.math.roundToInt + +object MessagesManager { + + fun setRootMaxWidth( + layout: View + ) { + val maxWidth = (AppGlobal.screenWidth * 0.7).roundToInt() + + if (layout is BoundedFrameLayout) { + layout.maxWidth = maxWidth + } else if (layout is BoundedLinearLayout) { + layout.maxWidth = maxWidth + } + } + + fun loadPhotos( + context: Context, + message: VkMessage, + photosContainer: LinearLayoutCompat + ) { + photosContainer.removeAllViews() + + message.attachments?.let { attachments -> + val photos = attachments.map { it as VkPhoto } + + photos.forEach { photo -> + val size = photo.sizeOfType('m') ?: return + + val newPhoto = ShapeableImageView(context).also { + it.layoutParams = LinearLayoutCompat.LayoutParams( + AndroidUtils.px(size.width).roundToInt(), + AndroidUtils.px(size.height).roundToInt() + ) + it.shapeAppearanceModel = + it.shapeAppearanceModel.withCornerSize { AndroidUtils.px(4) } + it.scaleType = ImageView.ScaleType.CENTER_CROP + } + + val spacer = Space(context).also { + it.layoutParams = LinearLayoutCompat.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + AndroidUtils.px(5).roundToInt() + ) + } + + if (photosContainer.isNotEmpty()) + photosContainer.addView(spacer) + + photosContainer.addView(newPhoto) + + newPhoto.load(size.url) + } + } + } + + fun loadMessageAvatar( + message: VkMessage, + messageUser: VkUser?, + messageGroup: VkGroup?, + imageView: ImageView + ) { + val avatar = when { + message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200 + message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200 + else -> null + } + + imageView.load(avatar) { crossfade(100) } + } + + fun setMessageText( + message: VkMessage, + textView: TextView + ) { + textView.text = message.text ?: "[no_message]" + } + + +} \ 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 new file mode 100644 index 00000000..6a99ef8c --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt @@ -0,0 +1,50 @@ +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 kotlinx.coroutines.* +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext + +class LongPollService { +} + +class LongPollTask @Inject constructor( + private val dataSource: MessagesDataSource, + private val longPollRepo: LongPollRepo +) : CoroutineScope { + + companion object { + const val TAG = "LongPollTask" + } + + private val job = SupervisorJob() + + private val exceptionHandler = CoroutineExceptionHandler { _, throwable -> + Log.d(TAG, "error: $throwable") + throwable.printStackTrace() + } + + override val coroutineContext: CoroutineContext + get() = Dispatchers.Default + job + exceptionHandler + + fun startPolling(): Job { + if (job.isCompleted || job.isCancelled) throw Exception("Job is over") + + return launch { + val serverInfo = dataSource.getLongPollServer( + MessagesGetLongPollServerRequest( + needPts = true, + version = 10 + ) + ) + + println("TESTJOPAAAAAA: $serverInfo") +// val response = serverInfo.response ?: return@launch + + + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/widget/CircleImageView.kt b/app/src/main/kotlin/com/meloda/fast/widget/CircleImageView.kt index 6492c7a2..3aa3579c 100644 --- a/app/src/main/kotlin/com/meloda/fast/widget/CircleImageView.kt +++ b/app/src/main/kotlin/com/meloda/fast/widget/CircleImageView.kt @@ -8,7 +8,6 @@ import android.util.AttributeSet import android.view.ViewTreeObserver import androidx.appcompat.widget.AppCompatImageView -// TODO: 8/31/2021 extend ShapeableImageView and set corners for half of size class CircleImageView : AppCompatImageView { companion object { @@ -27,7 +26,6 @@ class CircleImageView : AppCompatImageView { attrs, defStyleAttr ) { - init() } diff --git a/app/src/main/res/drawable/ic_message_in_background_middle.xml b/app/src/main/res/drawable/ic_message_in_background_middle.xml new file mode 100644 index 00000000..f44081fd --- /dev/null +++ b/app/src/main/res/drawable/ic_message_in_background_middle.xml @@ -0,0 +1,13 @@ + + + + + + + + \ 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 a8d5ce89..71d2120f 100644 --- a/app/src/main/res/drawable/ic_message_out_background.xml +++ b/app/src/main/res/drawable/ic_message_out_background.xml @@ -2,7 +2,7 @@ - + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_out_background_middle_stroke.xml b/app/src/main/res/drawable/ic_message_out_background_middle_stroke.xml new file mode 100644 index 00000000..bc6f96c0 --- /dev/null +++ b/app/src/main/res/drawable/ic_message_out_background_middle_stroke.xml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_out_background_stroke.xml b/app/src/main/res/drawable/ic_message_out_background_stroke.xml new file mode 100644 index 00000000..4e32fc40 --- /dev/null +++ b/app/src/main/res/drawable/ic_message_out_background_stroke.xml @@ -0,0 +1,13 @@ + + + + + + + + \ 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 732ade60..419ce0f6 100644 --- a/app/src/main/res/layout/fragment_conversations.xml +++ b/app/src/main/res/layout/fragment_conversations.xml @@ -14,12 +14,13 @@ app:elevation="0dp"> + android:layout_height="match_parent"> - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_photos_in.xml b/app/src/main/res/layout/item_message_attachment_photos_in.xml new file mode 100644 index 00000000..02e6ecf5 --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_photos_in.xml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_photo_in.xml b/app/src/main/res/layout/item_message_attachment_photos_out.xml similarity index 53% rename from app/src/main/res/layout/item_message_attachment_photo_in.xml rename to app/src/main/res/layout/item_message_attachment_photos_out.xml index eee4bff7..f3f33124 100644 --- a/app/src/main/res/layout/item_message_attachment_photo_in.xml +++ b/app/src/main/res/layout/item_message_attachment_photos_out.xml @@ -1,19 +1,18 @@ - + - - + android:layout_gravity="end" + android:orientation="vertical" /> - + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_sticker_in.xml b/app/src/main/res/layout/item_message_attachment_sticker_in.xml new file mode 100644 index 00000000..0866ccb4 --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_sticker_in.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_sticker_out.xml b/app/src/main/res/layout/item_message_attachment_sticker_out.xml new file mode 100644 index 00000000..53cd425e --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_sticker_out.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + \ 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 2a9fd45a..2cb64bf1 100644 --- a/app/src/main/res/layout/item_message_in.xml +++ b/app/src/main/res/layout/item_message_in.xml @@ -22,6 +22,12 @@ android:layout_height="wrap_content" android:orientation="vertical"> + + - + android:gravity="bottom" + android:orientation="horizontal"> - + android:background="@drawable/ic_message_in_background" + android:backgroundTint="@color/n2_100" + tools:ignore="UselessParent"> - + + + + + - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_out.xml b/app/src/main/res/layout/item_message_out.xml index b8fe6201..47042176 100644 --- a/app/src/main/res/layout/item_message_out.xml +++ b/app/src/main/res/layout/item_message_out.xml @@ -6,6 +6,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="end|bottom" + android:orientation="horizontal" android:paddingHorizontal="12dp" android:paddingVertical="2.5dp"> @@ -17,35 +18,46 @@ android:layout_marginBottom="20dp" android:src="@color/a3_200" /> - + android:orientation="vertical"> - + + + android:background="@drawable/ic_message_out_background_stroke" + android:padding="1.5dp" + tools:ignore="UselessParent"> - + android:layout_gravity="center" + android:background="@drawable/ic_message_out_background"> - - + + + + + + diff --git a/app/src/main/res/layout/item_message_service.xml b/app/src/main/res/layout/item_message_service.xml index 34d40b12..daf9fd72 100644 --- a/app/src/main/res/layout/item_message_service.xml +++ b/app/src/main/res/layout/item_message_service.xml @@ -5,6 +5,8 @@ + + \ No newline at end of file diff --git a/app/src/main/res/navigation/main.xml b/app/src/main/res/navigation/main.xml index ca936bea..91915b7b 100644 --- a/app/src/main/res/navigation/main.xml +++ b/app/src/main/res/navigation/main.xml @@ -13,7 +13,9 @@ + app:destination="@id/loginFragment" + app:popUpTo="@id/mainFragment" + app:popUpToInclusive="true" /> diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index e48bc8e7..0e9c1a79 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -1,8 +1,7 @@ + android:id="@+id/nav_graph"> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index bf8f5a92..9901966a 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -26,6 +26,9 @@ @color/a1_0 + @color/n2_100 + @color/n1_10 + #FFFFFF #B1C6FA #4184F5 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6d9189e0..5c1dccd1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -44,5 +44,11 @@ D Now Start typing here... + Input login + Input password + Input code + Validation required + Unknown error occurred + Authorization failed