From c77ebae57a79d99023f381d6f530ea73d357ebd9 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Wed, 4 Aug 2021 23:01:22 +0300 Subject: [PATCH] lil update --- app/build.gradle.kts | 5 +- app/src/main/AndroidManifest.xml | 1 + .../main/java/com/meloda/fast/api/VKAuth.kt | 9 +- .../main/java/com/meloda/fast/api/VKRepo.kt | 12 +- .../main/java/com/meloda/fast/api/VKUrls.kt | 7 ++ .../meloda/fast/api/model/VKConversation.kt | 11 +- .../java/com/meloda/fast/api/model/VKModel.kt | 3 +- .../api/model/response/MessagesResponse.kt | 13 ++ .../ResponseMessagesGetConversations.kt | 6 - .../java/com/meloda/fast/api/util/VKUtil.kt | 92 ++++++++------ .../com/meloda/fast/base/BaseVMFragment.kt | 8 +- .../meloda/fast/base/adapter/BaseAdapter.kt | 44 +++---- .../com/meloda/fast/base/adapter/BaseItem.kt | 3 + .../fast/base/adapter/EmptyHeaderAdapter.kt | 35 ++++++ .../com/meloda/fast/base/adapter/Holders.kt | 11 +- .../java/com/meloda/fast/common/AppGlobal.kt | 6 + .../fast/fragment/login/LoginFragment.kt | 113 ++++++++++-------- .../com/meloda/fast/fragment/login/LoginVM.kt | 30 +++-- .../fragment/messages/ConversationsAdapter.kt | 41 +++++++ .../messages/ConversationsFragment.kt | 66 +++++++++- .../fast/fragment/messages/ConversationsVM.kt | 13 ++ .../com/meloda/fast/util/KeyboardUtils.kt | 10 +- app/src/main/res/drawable-v21/ic_security.xml | 10 ++ app/src/main/res/layout/dialog_captcha.xml | 83 +++++++++++++ .../res/layout/fragment_conversations.xml | 54 +++++---- app/src/main/res/layout/fragment_login.xml | 12 +- app/src/main/res/layout/item_conversation.xml | 32 ++--- app/src/main/res/values/colors.xml | 2 +- app/src/main/res/values/strings.xml | 4 +- app/src/main/res/values/styles.xml | 14 +++ app/src/main/res/values/themes.xml | 17 +-- build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 33 files changed, 548 insertions(+), 223 deletions(-) create mode 100644 app/src/main/java/com/meloda/fast/api/VKUrls.kt create mode 100644 app/src/main/java/com/meloda/fast/api/model/response/MessagesResponse.kt delete mode 100644 app/src/main/java/com/meloda/fast/api/model/response/ResponseMessagesGetConversations.kt create mode 100644 app/src/main/java/com/meloda/fast/base/adapter/BaseItem.kt create mode 100644 app/src/main/java/com/meloda/fast/base/adapter/EmptyHeaderAdapter.kt create mode 100644 app/src/main/java/com/meloda/fast/fragment/messages/ConversationsAdapter.kt create mode 100644 app/src/main/java/com/meloda/fast/fragment/messages/ConversationsVM.kt create mode 100644 app/src/main/res/drawable-v21/ic_security.xml create mode 100644 app/src/main/res/layout/dialog_captcha.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 96b3c5be..156673c0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,6 +4,7 @@ plugins { id("kotlin-kapt") id("androidx.navigation.safeargs.kotlin") id("dagger.hilt.android.plugin") + id("kotlin-parcelize") } android { @@ -64,7 +65,7 @@ kapt { } dependencies { - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.20") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.5.21") coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") @@ -103,7 +104,7 @@ dependencies { implementation("com.github.yogacp:android-viewbinding:1.0.2") - implementation("io.coil-kt:coil:1.2.2") + implementation("io.coil-kt:coil:1.3.0") implementation("com.google.code.gson:gson:2.8.7") implementation("org.jsoup:jsoup:1.14.1") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6842b223..3529a735 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" + android:testOnly="false" android:theme="@style/AppTheme"> } \ No newline at end of file diff --git a/app/src/main/java/com/meloda/fast/api/VKUrls.kt b/app/src/main/java/com/meloda/fast/api/VKUrls.kt new file mode 100644 index 00000000..b00610f3 --- /dev/null +++ b/app/src/main/java/com/meloda/fast/api/VKUrls.kt @@ -0,0 +1,7 @@ +package com.meloda.fast.api + +object VKUrls { + + const val getConversations = "messages.getConversations" + +} \ No newline at end of file diff --git a/app/src/main/java/com/meloda/fast/api/model/VKConversation.kt b/app/src/main/java/com/meloda/fast/api/model/VKConversation.kt index 9f9cb9fb..7867b8de 100644 --- a/app/src/main/java/com/meloda/fast/api/model/VKConversation.kt +++ b/app/src/main/java/com/meloda/fast/api/model/VKConversation.kt @@ -39,7 +39,7 @@ class VKConversation() : VKModel(), Cloneable { var isNoSound: Boolean = false var membersCount: Int = 0 - var title: String = "" + var title: String? = null var pinnedMessage: VKMessage? = null @@ -84,6 +84,7 @@ class VKConversation() : VKModel(), Cloneable { o.optJSONObject("chat_settings")?.let { membersCount = it.optInt("members_count") title = it.optString("title") + if (title?.isBlank() == true) title = null it.optJSONObject("pinned_message")?.let { pinned -> pinnedMessage = VKMessage(pinned) @@ -109,13 +110,9 @@ class VKConversation() : VKModel(), Cloneable { fun isGroup() = type == Type.GROUP - override fun toString(): String { - return title - } + override fun toString() = title ?: "" - public override fun clone(): VKConversation { - return super.clone() as VKConversation - } + public override fun clone() = super.clone() as VKConversation enum class Type(val value: String) { NULL("null"), diff --git a/app/src/main/java/com/meloda/fast/api/model/VKModel.kt b/app/src/main/java/com/meloda/fast/api/model/VKModel.kt index 594cfc2d..e49eef52 100644 --- a/app/src/main/java/com/meloda/fast/api/model/VKModel.kt +++ b/app/src/main/java/com/meloda/fast/api/model/VKModel.kt @@ -1,8 +1,9 @@ package com.meloda.fast.api.model +import com.meloda.fast.base.adapter.BaseItem import java.io.Serializable -abstract class VKModel : Serializable { +abstract class VKModel : BaseItem(), Serializable { abstract val attachmentType: VKAttachments.Type diff --git a/app/src/main/java/com/meloda/fast/api/model/response/MessagesResponse.kt b/app/src/main/java/com/meloda/fast/api/model/response/MessagesResponse.kt new file mode 100644 index 00000000..46d10bdf --- /dev/null +++ b/app/src/main/java/com/meloda/fast/api/model/response/MessagesResponse.kt @@ -0,0 +1,13 @@ +package com.meloda.fast.api.model.response + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +class MessagesResponse( + val count: Int +) { +} + +@Parcelize +data class GetConversationsResponse(val a: String) : Parcelable +// TODO: 7/12/2021 use hilt for this like in LIR and make simple conversations' screen diff --git a/app/src/main/java/com/meloda/fast/api/model/response/ResponseMessagesGetConversations.kt b/app/src/main/java/com/meloda/fast/api/model/response/ResponseMessagesGetConversations.kt deleted file mode 100644 index f10dc799..00000000 --- a/app/src/main/java/com/meloda/fast/api/model/response/ResponseMessagesGetConversations.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.meloda.fast.api.model.response - -class ResponseMessagesGetConversations( - val count: Int -) { -} \ No newline at end of file diff --git a/app/src/main/java/com/meloda/fast/api/util/VKUtil.kt b/app/src/main/java/com/meloda/fast/api/util/VKUtil.kt index dcefd1c2..09200155 100644 --- a/app/src/main/java/com/meloda/fast/api/util/VKUtil.kt +++ b/app/src/main/java/com/meloda/fast/api/util/VKUtil.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.util import androidx.annotation.WorkerThread +import com.meloda.fast.api.model.* import org.json.JSONArray import org.json.JSONObject import java.text.SimpleDateFormat @@ -27,9 +28,9 @@ object VKUtil { } fun sortMessagesByDate( - values: ArrayList, + values: ArrayList, firstOnTop: Boolean - ): ArrayList { + ): ArrayList { values.sortWith { m1, m2 -> val d1 = m1.date val d2 = m2.date @@ -45,9 +46,9 @@ object VKUtil { } fun sortConversationsByDate( - values: ArrayList, + values: ArrayList, firstOnTop: Boolean - ): ArrayList { + ): ArrayList { values.sortWith { c1, c2 -> val d1 = c1.lastMessage.date val d2 = c2.lastMessage.date @@ -121,25 +122,29 @@ object VKUtil { } - fun getTitle(conversation: com.meloda.fast.api.model.VKConversation, peerUser: com.meloda.fast.api.model.VKUser?, peerGroup: com.meloda.fast.api.model.VKGroup?): String { + fun getTitle( + conversation: VKConversation, + peerUser: VKUser?, + peerGroup: VKGroup? + ): String { return when { - conversation.isUser() -> { - peerUser?.let { return it.toString() } ?: "" - } + conversation.isUser() -> peerUser?.let { return it.toString() } ?: "" - conversation.isGroup() -> { - peerGroup?.let { return it.name } ?: "" - } - conversation.isChat() -> { - conversation.title - } + conversation.isGroup() -> peerGroup?.let { return it.name } ?: "" + + + conversation.isChat() -> conversation.title ?: "" else -> "" } } - fun getMessageTitle(message: com.meloda.fast.api.model.VKMessage, fromUser: com.meloda.fast.api.model.VKUser?, fromGroup: com.meloda.fast.api.model.VKGroup?): String { + fun getMessageTitle( + message: VKMessage, + fromUser: VKUser?, + fromGroup: VKGroup? + ): String { return when { message.isFromUser() -> { fromUser?.let { return it.toString() } ?: "" @@ -153,7 +158,11 @@ object VKUtil { } } - fun getAvatar(conversation: com.meloda.fast.api.model.VKConversation, peerUser: com.meloda.fast.api.model.VKUser?, peerGroup: com.meloda.fast.api.model.VKGroup?): String { + fun getAvatar( + conversation: VKConversation, + peerUser: VKUser?, + peerGroup: VKGroup? + ): String { return when { conversation.isUser() -> { peerUser?.let { return it.photo200 } ?: "" @@ -171,7 +180,11 @@ object VKUtil { } } - fun getUserAvatar(message: com.meloda.fast.api.model.VKMessage, fromUser: com.meloda.fast.api.model.VKUser?, fromGroup: com.meloda.fast.api.model.VKGroup?): String { + fun getUserAvatar( + message: VKMessage, + fromUser: VKUser?, + fromGroup: VKGroup? + ): String { return when { message.isFromUser() -> { fromUser?.let { return it.photo100 } ?: "" @@ -185,7 +198,7 @@ object VKUtil { } } - fun getUserPhoto(user: com.meloda.fast.api.model.VKUser): String { + fun getUserPhoto(user: VKUser): String { if (user.photo200.isEmpty()) { if (user.photo100.isEmpty()) { if (user.photo50.isEmpty()) { @@ -201,7 +214,7 @@ object VKUtil { return "" } - fun getGroupPhoto(group: com.meloda.fast.api.model.VKGroup): String { + fun getGroupPhoto(group: VKGroup): String { if (group.photo200.isEmpty()) { if (group.photo100.isEmpty()) { if (group.photo50.isEmpty()) { @@ -218,26 +231,26 @@ object VKUtil { } - fun parseConversations(array: JSONArray): ArrayList { - val conversations = arrayListOf() + fun parseConversations(array: JSONArray): ArrayList { + val conversations = arrayListOf() for (i in 0 until array.length()) { - conversations.add(com.meloda.fast.api.model.VKConversation(array.optJSONObject(i))) + conversations.add(VKConversation(array.optJSONObject(i))) } return conversations } - fun parseMessages(array: JSONArray): ArrayList { - val messages = arrayListOf() + fun parseMessages(array: JSONArray): ArrayList { + val messages = arrayListOf() for (i in 0 until array.length()) { - messages.add(com.meloda.fast.api.model.VKMessage(array.optJSONObject(i))) + messages.add(VKMessage(array.optJSONObject(i))) } return messages } fun isMessageHasFlag(mask: Int, flagName: String): Boolean { - val o: Any? = com.meloda.fast.api.model.VKMessage.flags[flagName] + val o: Any? = VKMessage.flags[flagName] return if (o != null) { //has flag val flag = o as Int flag and mask > 0 @@ -248,8 +261,8 @@ object VKUtil { //fromUser and fromGroup are null @Deprecated("need to rewrite") @WorkerThread - fun parseLongPollMessage(array: JSONArray): com.meloda.fast.api.model.VKMessage { - val message = com.meloda.fast.api.model.VKMessage() + fun parseLongPollMessage(array: JSONArray): VKMessage { + val message = VKMessage() val id = array.optInt(1) val flags = array.optInt(2) @@ -276,33 +289,34 @@ object VKUtil { } if (it.has("source_act")) { - message.action = com.meloda.fast.api.model.VKMessageAction().also { action -> - action.type = com.meloda.fast.api.model.VKMessageAction.Type.fromString(it.optString("source_act")) + message.action = VKMessageAction().also { action -> + action.type = + VKMessageAction.Type.fromString(it.optString("source_act")) when (action.type) { - com.meloda.fast.api.model.VKMessageAction.Type.CHAT_CREATE -> { + VKMessageAction.Type.CHAT_CREATE -> { action.text = it.optString("source_text") } - com.meloda.fast.api.model.VKMessageAction.Type.TITLE_UPDATE -> { + VKMessageAction.Type.TITLE_UPDATE -> { action.oldText = it.optString("source_old_text") action.text = it.optString("source_text") } - com.meloda.fast.api.model.VKMessageAction.Type.PIN_MESSAGE -> { + VKMessageAction.Type.PIN_MESSAGE -> { action.memberId = it.optInt("source_mid") action.conversationMessageId = it.optInt("source_chat_local_id") it.optJSONObject("source_message")?.let { message -> - action.message = com.meloda.fast.api.model.VKMessage(message) + action.message = VKMessage(message) } } - com.meloda.fast.api.model.VKMessageAction.Type.UNPIN_MESSAGE -> { + VKMessageAction.Type.UNPIN_MESSAGE -> { action.memberId = it.optInt("source_mid") action.conversationMessageId = it.optInt("source_chat_local_id") } - com.meloda.fast.api.model.VKMessageAction.Type.INVITE_USER, - com.meloda.fast.api.model.VKMessageAction.Type.KICK_USER, - com.meloda.fast.api.model.VKMessageAction.Type.SCREENSHOT, - com.meloda.fast.api.model.VKMessageAction.Type.INVITE_USER_BY_CALL -> { + VKMessageAction.Type.INVITE_USER, + VKMessageAction.Type.KICK_USER, + VKMessageAction.Type.SCREENSHOT, + VKMessageAction.Type.INVITE_USER_BY_CALL -> { action.memberId = it.optInt("source_mid") } } diff --git a/app/src/main/java/com/meloda/fast/base/BaseVMFragment.kt b/app/src/main/java/com/meloda/fast/base/BaseVMFragment.kt index 108f7aee..e7c5de1c 100644 --- a/app/src/main/java/com/meloda/fast/base/BaseVMFragment.kt +++ b/app/src/main/java/com/meloda/fast/base/BaseVMFragment.kt @@ -6,8 +6,8 @@ import androidx.annotation.LayoutRes import androidx.lifecycle.lifecycleScope import com.meloda.fast.base.viewmodel.BaseVM import com.meloda.fast.base.viewmodel.VKEvent -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.onEach abstract class BaseVMFragment : BaseFragment { @@ -24,10 +24,6 @@ abstract class BaseVMFragment : BaseFragment { } } - protected open fun onEvent(event: VKEvent) { - when (event) { - - } - } + protected open fun onEvent(event: VKEvent) {} } \ No newline at end of file diff --git a/app/src/main/java/com/meloda/fast/base/adapter/BaseAdapter.kt b/app/src/main/java/com/meloda/fast/base/adapter/BaseAdapter.kt index 0fe1a806..5b922c2b 100644 --- a/app/src/main/java/com/meloda/fast/base/adapter/BaseAdapter.kt +++ b/app/src/main/java/com/meloda/fast/base/adapter/BaseAdapter.kt @@ -5,37 +5,25 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.AdapterView -import androidx.lifecycle.MutableLiveData -import androidx.recyclerview.widget.RecyclerView -import com.meloda.fast.base.BaseHolder -import com.meloda.fast.extensions.LiveDataExtensions.add -import com.meloda.fast.extensions.LiveDataExtensions.addAll -import com.meloda.fast.extensions.LiveDataExtensions.clear -import com.meloda.fast.extensions.LiveDataExtensions.get -import com.meloda.fast.extensions.LiveDataExtensions.isEmpty -import com.meloda.fast.extensions.LiveDataExtensions.isNotEmpty -import com.meloda.fast.extensions.LiveDataExtensions.plusAssign -import com.meloda.fast.extensions.LiveDataExtensions.remove -import com.meloda.fast.extensions.LiveDataExtensions.removeAll -import com.meloda.fast.extensions.LiveDataExtensions.removeAt -import com.meloda.fast.extensions.LiveDataExtensions.set -import com.meloda.fast.extensions.LiveDataExtensions.size +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter @Suppress("UNCHECKED_CAST", "unused", "MemberVisibilityCanBePrivate", "CanBeParameter") -abstract class BaseAdapter( +abstract class BaseAdapter( var context: Context, - values: ArrayList -) : RecyclerView.Adapter() { + values: ArrayList, + diffUtil: DiffUtil.ItemCallback +) : ListAdapter(diffUtil) { - val cleanValues = MutableLiveData>(arrayListOf()) - val values = MutableLiveData>(arrayListOf()) - - protected var inflater: LayoutInflater = LayoutInflater.from(context) + val cleanValues = arrayListOf() + val values = arrayListOf() init { - this.values.value = values + addAll(values) } + protected var inflater: LayoutInflater = LayoutInflater.from(context) + var itemClickListener: OnItemClickListener? = null var itemLongClickListener: OnItemLongClickListener? = null @@ -44,13 +32,13 @@ abstract class BaseAdapter( itemLongClickListener = null } - open fun getItem(position: Int): Item { + override fun getItem(position: Int): Item { return values[position] } fun add(position: Int, item: Item) { - values.add(item, position) - cleanValues.add(item, position) + values.add(position, item) + cleanValues.add(position, item) } fun add(item: Item) { @@ -64,8 +52,8 @@ abstract class BaseAdapter( } fun addAll(position: Int, items: List) { - values.addAll(items, position) - cleanValues.addAll(items, position) + values.addAll(position, items) + cleanValues.addAll(position, items) } fun removeAll(items: List) { diff --git a/app/src/main/java/com/meloda/fast/base/adapter/BaseItem.kt b/app/src/main/java/com/meloda/fast/base/adapter/BaseItem.kt new file mode 100644 index 00000000..7b1630b1 --- /dev/null +++ b/app/src/main/java/com/meloda/fast/base/adapter/BaseItem.kt @@ -0,0 +1,3 @@ +package com.meloda.fast.base.adapter + +abstract class BaseItem \ No newline at end of file diff --git a/app/src/main/java/com/meloda/fast/base/adapter/EmptyHeaderAdapter.kt b/app/src/main/java/com/meloda/fast/base/adapter/EmptyHeaderAdapter.kt new file mode 100644 index 00000000..6da60751 --- /dev/null +++ b/app/src/main/java/com/meloda/fast/base/adapter/EmptyHeaderAdapter.kt @@ -0,0 +1,35 @@ +package com.meloda.fast.base.adapter + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isInvisible +import androidx.recyclerview.widget.RecyclerView +import com.meloda.fast.util.AndroidUtils +import kotlin.math.roundToInt + +class EmptyHeaderAdapter( + var context: Context +) : RecyclerView.Adapter() { + + inner class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = Holder(generateHeaderView()) + + override fun onBindViewHolder(holder: Holder, position: Int) { + } + + override fun getItemCount() = 1 + + private fun generateHeaderView() = View(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + AndroidUtils.px(56).roundToInt() + ) + isClickable = false + isEnabled = false + isFocusable = false + isInvisible = true + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/meloda/fast/base/adapter/Holders.kt b/app/src/main/java/com/meloda/fast/base/adapter/Holders.kt index 563da0a7..eb6989ba 100644 --- a/app/src/main/java/com/meloda/fast/base/adapter/Holders.kt +++ b/app/src/main/java/com/meloda/fast/base/adapter/Holders.kt @@ -1,14 +1,15 @@ -package com.meloda.fast.base +package com.meloda.fast.base.adapter import android.view.View import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding abstract class BaseHolder(v: View) : RecyclerView.ViewHolder(v) { - open fun bind(position: Int) { - bind(position, mutableListOf()) - } + open fun bind(position: Int) {} open fun bind(position: Int, payloads: MutableList?) {} -} \ No newline at end of file +} + +abstract class BindingHolder(protected val binding: B) : BaseHolder(binding.root) \ No newline at end of file diff --git a/app/src/main/java/com/meloda/fast/common/AppGlobal.kt b/app/src/main/java/com/meloda/fast/common/AppGlobal.kt index 053bfd5e..4f79d1cf 100644 --- a/app/src/main/java/com/meloda/fast/common/AppGlobal.kt +++ b/app/src/main/java/com/meloda/fast/common/AppGlobal.kt @@ -2,11 +2,13 @@ package com.meloda.fast.common import android.annotation.SuppressLint import android.app.Application +import android.content.Context import android.content.SharedPreferences import android.content.pm.PackageManager import android.content.res.Resources import android.database.sqlite.SQLiteDatabase import android.os.Handler +import android.view.inputmethod.InputMethodManager import androidx.core.content.pm.PackageInfoCompat import androidx.preference.PreferenceManager import com.meloda.fast.BuildConfig @@ -34,6 +36,8 @@ class AppGlobal : Application() { companion object { + lateinit var inputMethodManager: InputMethodManager + lateinit var preferences: SharedPreferences lateinit var locale: Locale lateinit var handler: Handler @@ -82,6 +86,8 @@ class AppGlobal : Application() { screenWidth = AndroidUtils.getDisplayWidth() screenHeight = AndroidUtils.getDisplayHeight() + + inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager } } \ No newline at end of file diff --git a/app/src/main/java/com/meloda/fast/fragment/login/LoginFragment.kt b/app/src/main/java/com/meloda/fast/fragment/login/LoginFragment.kt index 24cc6287..23d1b950 100644 --- a/app/src/main/java/com/meloda/fast/fragment/login/LoginFragment.kt +++ b/app/src/main/java/com/meloda/fast/fragment/login/LoginFragment.kt @@ -2,14 +2,11 @@ package com.meloda.fast.fragment.login import android.annotation.SuppressLint import android.os.Bundle -import android.view.Gravity import android.view.KeyEvent import android.view.View -import android.view.ViewGroup import android.view.inputmethod.EditorInfo import android.viewbinding.library.fragment.viewBinding import android.webkit.CookieManager -import android.widget.LinearLayout import androidx.appcompat.app.AlertDialog import androidx.core.os.bundleOf import androidx.core.view.isVisible @@ -19,14 +16,15 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import coil.load -import com.google.android.material.imageview.ShapeableImageView -import com.google.android.material.textfield.TextInputEditText +import coil.transform.RoundedCornersTransformation +import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputLayout import com.meloda.fast.R import com.meloda.fast.base.BaseVMFragment 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.databinding.DialogCaptchaBinding import com.meloda.fast.databinding.FragmentLoginBinding import com.meloda.fast.fragment.main.MainFragment import com.meloda.fast.util.KeyboardUtils @@ -36,7 +34,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.util.* import kotlin.concurrent.schedule -import kotlin.math.roundToInt @AndroidEntryPoint class LoginFragment : BaseVMFragment(R.layout.fragment_login) { @@ -48,6 +45,7 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { private var lastPassword: String = "" private var errorTimer: Timer? = null + private var captchaInputLayout: TextInputLayout? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -56,20 +54,28 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { prepareViews() + binding.loginInput.clearFocus() + setFragmentResultListener("validation") { _, bundle -> lifecycleScope.launch { viewModel.getValidatedData(bundle) } } + +// showCaptchaDialog( +// "https://www.vets4pets.com/syssiteassets/species/cat/kitten/tiny-kitten-in-field.jpg?width=1040", +// "" +// ) } override fun onEvent(event: VKEvent) { super.onEvent(event) when (event) { + is ShowError -> showErrorSnackbar(event.errorDescription) is ShowCaptchaDialog -> showCaptchaDialog(event.captchaImage, event.captchaSid) is GoToValidationEvent -> goToValidation(event.redirectUrl) is GoToMainEvent -> goToMain(event.haveAuthorized) StartProgressEvent -> onProgressStarted() - StopProgressEvent -> onProgressEnded() + StopProgressEvent -> onProgressStopped() } } @@ -80,7 +86,7 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { binding.progress.isVisible = true } - private fun onProgressEnded() { + private fun onProgressStopped() { binding.loginContainer.isVisible = true binding.passwordContainer.isVisible = true binding.auth.isVisible = true @@ -150,7 +156,7 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { if (!validateInputData(loginString, passwordString)) return@setOnClickListener - KeyboardUtils.hideKeyboardFrom(it) + KeyboardUtils.hideKeyboardFrom(requireView().findFocus()) lifecycleScope.launch { viewModel.login( @@ -162,19 +168,29 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { } } - private fun validateInputData(loginString: String, passwordString: String): Boolean { + // TODO: 7/27/2021 extract strings to resources + private fun validateInputData( + loginString: String?, + passwordString: String?, + captchaCode: String? = null + ): Boolean { var isValidated = true - if (loginString.isEmpty()) { + if (loginString?.isEmpty() == true) { isValidated = false setError("Input login", binding.loginLayout) } - if (passwordString.isEmpty()) { + if (passwordString?.isEmpty() == true) { isValidated = false setError("Input password", binding.passwordLayout) } + if (captchaCode?.isEmpty() == true && captchaInputLayout != null) { + isValidated = false + setError("Input code", captchaInputLayout!!) + } + return isValidated } @@ -198,59 +214,60 @@ class LoginFragment : BaseVMFragment(R.layout.fragment_login) { private fun clearErrors() { binding.loginLayout.error = "" binding.passwordLayout.error = "" + + captchaInputLayout?.error = "" } - // TODO: 7/10/2021 extract layout to resources private fun showCaptchaDialog(captchaImage: String, captchaSid: String) { - val metrics = resources.displayMetrics + val captchaBinding = DialogCaptchaBinding.inflate(layoutInflater, null, false) + captchaInputLayout = captchaBinding.captchaLayout - val width = (metrics.widthPixels / 3.5).roundToInt() - val height = metrics.heightPixels / 7 - - val image = ShapeableImageView(requireContext()).also { - it.layoutParams = ViewGroup.LayoutParams(width, height) + captchaBinding.image.load(captchaImage) { + crossfade(100) + transformations(RoundedCornersTransformation(4f)) } - val shapeModel = image.shapeAppearanceModel - image.shapeAppearanceModel = shapeModel.withCornerSize { 12f } - - image.load(captchaImage) { crossfade(100) } - - val captchaCodeEditText = TextInputEditText(requireContext()) - captchaCodeEditText.setHint(R.string.captcha_hint) - - captchaCodeEditText.layoutParams = - LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - val builder = AlertDialog.Builder(requireContext()) + .setView(captchaBinding.root) + .setCancelable(false) + .setTitle(R.string.input_captcha) - val layout = LinearLayout(requireContext()) + val dialog = builder.show() - layout.orientation = LinearLayout.VERTICAL - layout.gravity = Gravity.CENTER - layout.addView(image) - layout.addView(captchaCodeEditText) + captchaBinding.ok.setOnClickListener { + val captchaCode = captchaBinding.captchaInput.text.toString().trim() - builder.setView(layout) - builder.setNegativeButton(android.R.string.cancel, null) - builder.setPositiveButton(android.R.string.ok) { _, _ -> - val captchaCode = captchaCodeEditText.text.toString().trim() + if (!validateInputData( + loginString = null, + passwordString = null, + captchaCode = captchaCode + ) + ) return@setOnClickListener + + dialog.dismiss() lifecycleScope.launch { viewModel.login( - binding.webView, - lastEmail, - lastPassword, - "&captcha_sid=$captchaSid&captcha_key=$captchaCode" + webView = binding.webView, + email = lastEmail, + password = lastPassword, + captchaSid = captchaSid, + captchaKey = captchaCode ) } } + captchaBinding.cancel.setOnClickListener { dialog.dismiss() } + } - builder.setTitle(R.string.input_captcha) - builder.show() + private fun showErrorSnackbar(errorDescription: String) { + val snackbar = Snackbar.make( + requireView(), + getString(R.string.error, errorDescription), + Snackbar.LENGTH_LONG + ) + + snackbar.animationMode = Snackbar.ANIMATION_MODE_FADE + snackbar.show() } private fun goToValidation(redirectUrl: String) { diff --git a/app/src/main/java/com/meloda/fast/fragment/login/LoginVM.kt b/app/src/main/java/com/meloda/fast/fragment/login/LoginVM.kt index 330fabdd..7f449236 100644 --- a/app/src/main/java/com/meloda/fast/fragment/login/LoginVM.kt +++ b/app/src/main/java/com/meloda/fast/fragment/login/LoginVM.kt @@ -1,6 +1,7 @@ package com.meloda.fast.fragment.login import android.os.Bundle +import android.util.Log import android.webkit.JavascriptInterface import android.webkit.WebView import android.webkit.WebViewClient @@ -19,20 +20,21 @@ import org.jsoup.Jsoup class LoginVM : BaseVM() { - private var isWebViewPrepared = true + private var isWebViewPrepared = false suspend fun login( webView: WebView, email: String, password: String, - captcha: String = "" + captchaSid: String? = null, + captchaKey: String? = null ) { sendEvent(StartProgressEvent) - val urlToGo = VKAuth.getDirectAuthUrl(email, password, captcha) + val urlToGo = VKAuth.getDirectAuthUrl(email, password, captchaSid, captchaKey) - if (isWebViewPrepared) { - isWebViewPrepared = false + if (!isWebViewPrepared) { + isWebViewPrepared = true webView.addJavascriptInterface(WebViewHandlerInterface(), "HtmlHandler") @@ -52,11 +54,14 @@ class LoginVM : BaseVM() { @Suppress("MoveVariableDeclarationIntoWhen") private fun checkResponse(response: JSONObject) { viewModelScope.launch(Dispatchers.Default) { - delay(1500) - sendEvent(StopProgressEvent) - if (response.has("error")) { + sendEvent(StopProgressEvent) + val errorString = response.optString("error") + val errorDescription = response.optString("error_description") + + // TODO: 7/27/2021 use this with localized resources +// val errorType = response.optString("error_type") when (errorString) { "need_validation" -> { @@ -68,10 +73,18 @@ class LoginVM : BaseVM() { val captchaImage = response.optString("captcha_img") val captchaSid = response.optString("captcha_sid") + Log.d("CAPTCHA", "captchaImage: $captchaImage") + tasksEventChannel.send(ShowCaptchaDialog(captchaImage, captchaSid)) } + else -> { + tasksEventChannel.send(ShowError(errorDescription)) + } } } else { + delay(1500) + sendEvent(StopProgressEvent) + val userId = response.optInt("user_id", -1) val accessToken = response.optString("access_token") @@ -108,6 +121,7 @@ class LoginVM : BaseVM() { } +data class ShowError(val errorDescription: String) : VKEvent() data class ShowCaptchaDialog(val captchaImage: String, val captchaSid: String) : VKEvent() data class GoToValidationEvent(val redirectUrl: String) : VKEvent() data class GoToMainEvent(val haveAuthorized: Boolean = true) : VKEvent() \ No newline at end of file diff --git a/app/src/main/java/com/meloda/fast/fragment/messages/ConversationsAdapter.kt b/app/src/main/java/com/meloda/fast/fragment/messages/ConversationsAdapter.kt new file mode 100644 index 00000000..de34a4f1 --- /dev/null +++ b/app/src/main/java/com/meloda/fast/fragment/messages/ConversationsAdapter.kt @@ -0,0 +1,41 @@ +package com.meloda.fast.fragment.messages + +import android.content.Context +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import com.meloda.fast.api.model.VKConversation +import com.meloda.fast.base.adapter.BaseAdapter +import com.meloda.fast.base.adapter.BindingHolder +import com.meloda.fast.databinding.ItemConversationBinding + +class ConversationsAdapter(context: Context, values: ArrayList) : + BaseAdapter( + context, values, COMPARATOR + ) { + + companion object { + private val COMPARATOR = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame( + oldItem: VKConversation, + newItem: VKConversation + ) = false + + override fun areContentsTheSame( + oldItem: VKConversation, + newItem: VKConversation + ) = false + } + } + + inner class ItemHolder(binding: ItemConversationBinding) : + BindingHolder(binding) { + + override fun bind(position: Int) { + binding.title.text = getItem(position).title ?: "HUI" + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = + ItemHolder(ItemConversationBinding.inflate(inflater, parent, false)) + +} \ No newline at end of file diff --git a/app/src/main/java/com/meloda/fast/fragment/messages/ConversationsFragment.kt b/app/src/main/java/com/meloda/fast/fragment/messages/ConversationsFragment.kt index d9d5a21f..d723b47b 100644 --- a/app/src/main/java/com/meloda/fast/fragment/messages/ConversationsFragment.kt +++ b/app/src/main/java/com/meloda/fast/fragment/messages/ConversationsFragment.kt @@ -3,19 +3,81 @@ package com.meloda.fast.fragment.messages import android.os.Bundle import android.view.View import android.viewbinding.library.fragment.viewBinding +import androidx.core.view.isVisible +import androidx.fragment.app.viewModels import com.meloda.fast.R -import com.meloda.fast.base.BaseFragment +import com.meloda.fast.base.BaseVMFragment +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.databinding.FragmentConversationsBinding +import com.meloda.fast.util.AndroidUtils import dagger.hilt.android.AndroidEntryPoint +import kotlin.math.roundToInt @AndroidEntryPoint -class ConversationsFragment : BaseFragment(R.layout.fragment_conversations) { +class ConversationsFragment : BaseVMFragment(R.layout.fragment_conversations) { + override val viewModel: ConversationsVM by viewModels() private val binding: FragmentConversationsBinding by viewBinding() + private lateinit var adapter: ConversationsAdapter + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + prepareViews() + + viewModel.loadConversations() + } + + override fun onEvent(event: VKEvent) { + super.onEvent(event) + when (event) { + StartProgressEvent -> onProgressStarted() + StopProgressEvent -> onProgressStopped() + } + } + + private fun onProgressStarted() { + if (adapter.isEmpty()) + binding.progressBar.isVisible = true + } + + private fun onProgressStopped() { + binding.progressBar.isVisible = false + } + + private fun prepareViews() { + prepareRecyclerView() + prepareRefreshLayout() + } + + private fun prepareRecyclerView() { + + } + + private fun prepareRefreshLayout() { + with(binding.refreshLayout) { + setProgressViewOffset( + true, + AndroidUtils.px(40).roundToInt(), + AndroidUtils.px(96).roundToInt() + ) + setProgressBackgroundColorSchemeColor( + AndroidUtils.getThemeAttrColor( + requireContext(), + R.attr.colorSurface + ) + ) + setColorSchemeColors( + AndroidUtils.getThemeAttrColor( + requireContext(), + R.attr.colorAccent + ) + ) + setOnRefreshListener { } + } } } \ No newline at end of file diff --git a/app/src/main/java/com/meloda/fast/fragment/messages/ConversationsVM.kt b/app/src/main/java/com/meloda/fast/fragment/messages/ConversationsVM.kt new file mode 100644 index 00000000..5dc00b15 --- /dev/null +++ b/app/src/main/java/com/meloda/fast/fragment/messages/ConversationsVM.kt @@ -0,0 +1,13 @@ +package com.meloda.fast.fragment.messages + +import androidx.lifecycle.viewModelScope +import com.meloda.fast.base.viewmodel.BaseVM +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ConversationsVM : BaseVM() { + + fun loadConversations() = viewModelScope.launch(Dispatchers.Default) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/meloda/fast/util/KeyboardUtils.kt b/app/src/main/java/com/meloda/fast/util/KeyboardUtils.kt index 8b31c442..2c76a379 100644 --- a/app/src/main/java/com/meloda/fast/util/KeyboardUtils.kt +++ b/app/src/main/java/com/meloda/fast/util/KeyboardUtils.kt @@ -1,16 +1,16 @@ package com.meloda.fast.util import android.view.View +import com.meloda.fast.common.AppGlobal -//TODO object KeyboardUtils { - fun hideKeyboardFrom(view: View) { -// AppGlobal.inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) + fun hideKeyboardFrom(focusedView: View?) { + AppGlobal.inputMethodManager.hideSoftInputFromWindow(focusedView?.windowToken, 0) } - fun showKeyboard(focusedView: View) { -// AppGlobal.inputMethodManager.showSoftInput(focusedView, 0) + fun showKeyboard(viewToFocus: View) { + AppGlobal.inputMethodManager.showSoftInput(viewToFocus, 0) } } \ No newline at end of file diff --git a/app/src/main/res/drawable-v21/ic_security.xml b/app/src/main/res/drawable-v21/ic_security.xml new file mode 100644 index 00000000..ef2e5521 --- /dev/null +++ b/app/src/main/res/drawable-v21/ic_security.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_captcha.xml b/app/src/main/res/layout/dialog_captcha.xml new file mode 100644 index 00000000..cbdb0817 --- /dev/null +++ b/app/src/main/res/layout/dialog_captcha.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 81454c3e..4c3f8723 100644 --- a/app/src/main/res/layout/fragment_conversations.xml +++ b/app/src/main/res/layout/fragment_conversations.xml @@ -1,45 +1,47 @@ - - - + + + + + + - - - - - - - - - + android:background="@drawable/toolbar_background" + android:elevation="3dp" + app:title="@string/conversations" + app:titleCentered="true" /> + android:visibility="gone" + tools:visibility="visible" /> + android:layout_height="wrap_content" + app:boxStrokeErrorColor="@android:color/transparent"> + android:inputType="textEmailAddress" /> - @@ -117,9 +117,9 @@ android:id="@+id/passwordInput" android:layout_width="match_parent" android:layout_height="48dp" + android:imeOptions="actionGo" android:hint="@string/password_login_hint" - android:inputType="textPassword" - android:textCursorDrawable="@null" /> + android:inputType="textPassword" /> diff --git a/app/src/main/res/layout/item_conversation.xml b/app/src/main/res/layout/item_conversation.xml index 8a35cf3b..62273add 100644 --- a/app/src/main/res/layout/item_conversation.xml +++ b/app/src/main/res/layout/item_conversation.xml @@ -2,7 +2,7 @@ - #ffffff + @color/accent #ffffff #4284F4 #ffffff diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a4e55e1..3a35b892 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -150,9 +150,11 @@ Password E-mail or phone number Log in - Captcha + Captcha code Input code from picture Login + Conversations + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 4fc6a9bc..20390357 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,5 +1,6 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 7647c116..fb58fea1 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -27,6 +27,7 @@ @android:color/transparent +