lil update

This commit is contained in:
2021-08-04 23:01:22 +03:00
parent dabf2f86fd
commit c77ebae57a
33 changed files with 548 additions and 223 deletions
+3 -2
View File
@@ -4,6 +4,7 @@ plugins {
id("kotlin-kapt") id("kotlin-kapt")
id("androidx.navigation.safeargs.kotlin") id("androidx.navigation.safeargs.kotlin")
id("dagger.hilt.android.plugin") id("dagger.hilt.android.plugin")
id("kotlin-parcelize")
} }
android { android {
@@ -64,7 +65,7 @@ kapt {
} }
dependencies { 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") 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("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("com.google.code.gson:gson:2.8.7")
implementation("org.jsoup:jsoup:1.14.1") implementation("org.jsoup:jsoup:1.14.1")
+1
View File
@@ -12,6 +12,7 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:testOnly="false"
android:theme="@style/AppTheme"> android:theme="@style/AppTheme">
<activity <activity
android:name=".activity.MainActivity" android:name=".activity.MainActivity"
@@ -27,14 +27,19 @@ object VKAuth {
const val redirectUrl = "https://oauth.vk.com/blank.html" const val redirectUrl = "https://oauth.vk.com/blank.html"
fun getDirectAuthUrl(login: String, password: String, captcha: String = ""): String { fun getDirectAuthUrl(
login: String,
password: String,
captchaSid: String? = null,
captchaKey: String? = null
): String {
return "https://oauth.vk.com/token?grant_type=password&" + return "https://oauth.vk.com/token?grant_type=password&" +
"client_id=${VKConstants.VK_APP_ID}&" + "client_id=${VKConstants.VK_APP_ID}&" +
"scope=$settings&" + "scope=$settings&" +
"client_secret=${VKConstants.VK_APP_SECRET}&" + "client_secret=${VKConstants.VK_APP_SECRET}&" +
"username=$login&" + "username=$login&" +
"password=$password" + "password=$password" +
(if (captcha.isBlank()) "" else "&$captcha") + (if (captchaSid == null || captchaKey == null) "" else "&captcha_sid=$captchaSid&captcha_key=$captchaKey") +
"&v=${VKApi.API_VERSION}" "&v=${VKApi.API_VERSION}"
// return "https://oauth.vk.com/token?grant_type=password&" + // return "https://oauth.vk.com/token?grant_type=password&" +
// "client_id=2274003&" + // "client_id=2274003&" +
@@ -1,7 +1,17 @@
package com.meloda.fast.api package com.meloda.fast.api
import com.meloda.fast.api.model.response.GetConversationsResponse
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface VKRepo { interface VKRepo {
@FormUrlEncoded
@POST(VKUrls.getConversations)
suspend fun getAllChats(
@Field("user_id") chatId: Int,
@Field("token") token: String
): Answer<GetConversationsResponse>
} }
@@ -0,0 +1,7 @@
package com.meloda.fast.api
object VKUrls {
const val getConversations = "messages.getConversations"
}
@@ -39,7 +39,7 @@ class VKConversation() : VKModel(), Cloneable {
var isNoSound: Boolean = false var isNoSound: Boolean = false
var membersCount: Int = 0 var membersCount: Int = 0
var title: String = "" var title: String? = null
var pinnedMessage: VKMessage? = null var pinnedMessage: VKMessage? = null
@@ -84,6 +84,7 @@ class VKConversation() : VKModel(), Cloneable {
o.optJSONObject("chat_settings")?.let { o.optJSONObject("chat_settings")?.let {
membersCount = it.optInt("members_count") membersCount = it.optInt("members_count")
title = it.optString("title") title = it.optString("title")
if (title?.isBlank() == true) title = null
it.optJSONObject("pinned_message")?.let { pinned -> it.optJSONObject("pinned_message")?.let { pinned ->
pinnedMessage = VKMessage(pinned) pinnedMessage = VKMessage(pinned)
@@ -109,13 +110,9 @@ class VKConversation() : VKModel(), Cloneable {
fun isGroup() = type == Type.GROUP fun isGroup() = type == Type.GROUP
override fun toString(): String { override fun toString() = title ?: ""
return title
}
public override fun clone(): VKConversation { public override fun clone() = super.clone() as VKConversation
return super.clone() as VKConversation
}
enum class Type(val value: String) { enum class Type(val value: String) {
NULL("null"), NULL("null"),
@@ -1,8 +1,9 @@
package com.meloda.fast.api.model package com.meloda.fast.api.model
import com.meloda.fast.base.adapter.BaseItem
import java.io.Serializable import java.io.Serializable
abstract class VKModel : Serializable { abstract class VKModel : BaseItem(), Serializable {
abstract val attachmentType: VKAttachments.Type abstract val attachmentType: VKAttachments.Type
@@ -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
@@ -1,6 +0,0 @@
package com.meloda.fast.api.model.response
class ResponseMessagesGetConversations(
val count: Int
) {
}
@@ -1,6 +1,7 @@
package com.meloda.fast.api.util package com.meloda.fast.api.util
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import com.meloda.fast.api.model.*
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@@ -27,9 +28,9 @@ object VKUtil {
} }
fun sortMessagesByDate( fun sortMessagesByDate(
values: ArrayList<com.meloda.fast.api.model.VKMessage>, values: ArrayList<VKMessage>,
firstOnTop: Boolean firstOnTop: Boolean
): ArrayList<com.meloda.fast.api.model.VKMessage> { ): ArrayList<VKMessage> {
values.sortWith { m1, m2 -> values.sortWith { m1, m2 ->
val d1 = m1.date val d1 = m1.date
val d2 = m2.date val d2 = m2.date
@@ -45,9 +46,9 @@ object VKUtil {
} }
fun sortConversationsByDate( fun sortConversationsByDate(
values: ArrayList<com.meloda.fast.api.model.VKConversation>, values: ArrayList<VKConversation>,
firstOnTop: Boolean firstOnTop: Boolean
): ArrayList<com.meloda.fast.api.model.VKConversation> { ): ArrayList<VKConversation> {
values.sortWith { c1, c2 -> values.sortWith { c1, c2 ->
val d1 = c1.lastMessage.date val d1 = c1.lastMessage.date
val d2 = c2.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 { return when {
conversation.isUser() -> { conversation.isUser() -> peerUser?.let { return it.toString() } ?: ""
peerUser?.let { return it.toString() } ?: ""
}
conversation.isGroup() -> {
peerGroup?.let { return it.name } ?: ""
}
conversation.isChat() -> { conversation.isGroup() -> peerGroup?.let { return it.name } ?: ""
conversation.title
}
conversation.isChat() -> conversation.title ?: ""
else -> "" 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 { return when {
message.isFromUser() -> { message.isFromUser() -> {
fromUser?.let { return it.toString() } ?: "" 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 { return when {
conversation.isUser() -> { conversation.isUser() -> {
peerUser?.let { return it.photo200 } ?: "" 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 { return when {
message.isFromUser() -> { message.isFromUser() -> {
fromUser?.let { return it.photo100 } ?: "" 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.photo200.isEmpty()) {
if (user.photo100.isEmpty()) { if (user.photo100.isEmpty()) {
if (user.photo50.isEmpty()) { if (user.photo50.isEmpty()) {
@@ -201,7 +214,7 @@ object VKUtil {
return "" return ""
} }
fun getGroupPhoto(group: com.meloda.fast.api.model.VKGroup): String { fun getGroupPhoto(group: VKGroup): String {
if (group.photo200.isEmpty()) { if (group.photo200.isEmpty()) {
if (group.photo100.isEmpty()) { if (group.photo100.isEmpty()) {
if (group.photo50.isEmpty()) { if (group.photo50.isEmpty()) {
@@ -218,26 +231,26 @@ object VKUtil {
} }
fun parseConversations(array: JSONArray): ArrayList<com.meloda.fast.api.model.VKConversation> { fun parseConversations(array: JSONArray): ArrayList<VKConversation> {
val conversations = arrayListOf<com.meloda.fast.api.model.VKConversation>() val conversations = arrayListOf<VKConversation>()
for (i in 0 until array.length()) { 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 return conversations
} }
fun parseMessages(array: JSONArray): ArrayList<com.meloda.fast.api.model.VKMessage> { fun parseMessages(array: JSONArray): ArrayList<VKMessage> {
val messages = arrayListOf<com.meloda.fast.api.model.VKMessage>() val messages = arrayListOf<VKMessage>()
for (i in 0 until array.length()) { 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 return messages
} }
fun isMessageHasFlag(mask: Int, flagName: String): Boolean { 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 return if (o != null) { //has flag
val flag = o as Int val flag = o as Int
flag and mask > 0 flag and mask > 0
@@ -248,8 +261,8 @@ object VKUtil {
//fromUser and fromGroup are null //fromUser and fromGroup are null
@Deprecated("need to rewrite") @Deprecated("need to rewrite")
@WorkerThread @WorkerThread
fun parseLongPollMessage(array: JSONArray): com.meloda.fast.api.model.VKMessage { fun parseLongPollMessage(array: JSONArray): VKMessage {
val message = com.meloda.fast.api.model.VKMessage() val message = VKMessage()
val id = array.optInt(1) val id = array.optInt(1)
val flags = array.optInt(2) val flags = array.optInt(2)
@@ -276,33 +289,34 @@ object VKUtil {
} }
if (it.has("source_act")) { if (it.has("source_act")) {
message.action = com.meloda.fast.api.model.VKMessageAction().also { action -> message.action = VKMessageAction().also { action ->
action.type = com.meloda.fast.api.model.VKMessageAction.Type.fromString(it.optString("source_act")) action.type =
VKMessageAction.Type.fromString(it.optString("source_act"))
when (action.type) { when (action.type) {
com.meloda.fast.api.model.VKMessageAction.Type.CHAT_CREATE -> { VKMessageAction.Type.CHAT_CREATE -> {
action.text = it.optString("source_text") 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.oldText = it.optString("source_old_text")
action.text = it.optString("source_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.memberId = it.optInt("source_mid")
action.conversationMessageId = it.optInt("source_chat_local_id") action.conversationMessageId = it.optInt("source_chat_local_id")
it.optJSONObject("source_message")?.let { message -> 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.memberId = it.optInt("source_mid")
action.conversationMessageId = it.optInt("source_chat_local_id") action.conversationMessageId = it.optInt("source_chat_local_id")
} }
com.meloda.fast.api.model.VKMessageAction.Type.INVITE_USER, VKMessageAction.Type.INVITE_USER,
com.meloda.fast.api.model.VKMessageAction.Type.KICK_USER, VKMessageAction.Type.KICK_USER,
com.meloda.fast.api.model.VKMessageAction.Type.SCREENSHOT, VKMessageAction.Type.SCREENSHOT,
com.meloda.fast.api.model.VKMessageAction.Type.INVITE_USER_BY_CALL -> { VKMessageAction.Type.INVITE_USER_BY_CALL -> {
action.memberId = it.optInt("source_mid") action.memberId = it.optInt("source_mid")
} }
} }
@@ -6,8 +6,8 @@ import androidx.annotation.LayoutRes
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.meloda.fast.base.viewmodel.BaseVM import com.meloda.fast.base.viewmodel.BaseVM
import com.meloda.fast.base.viewmodel.VKEvent import com.meloda.fast.base.viewmodel.VKEvent
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.onEach
abstract class BaseVMFragment<VM : BaseVM> : BaseFragment { abstract class BaseVMFragment<VM : BaseVM> : BaseFragment {
@@ -24,10 +24,6 @@ abstract class BaseVMFragment<VM : BaseVM> : BaseFragment {
} }
} }
protected open fun onEvent(event: VKEvent) { protected open fun onEvent(event: VKEvent) {}
when (event) {
}
}
} }
@@ -5,37 +5,25 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView import android.widget.AdapterView
import androidx.lifecycle.MutableLiveData import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.ListAdapter
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
@Suppress("UNCHECKED_CAST", "unused", "MemberVisibilityCanBePrivate", "CanBeParameter") @Suppress("UNCHECKED_CAST", "unused", "MemberVisibilityCanBePrivate", "CanBeParameter")
abstract class BaseAdapter<Item, VH : BaseHolder>( abstract class BaseAdapter<Item : BaseItem, VH : BaseHolder>(
var context: Context, var context: Context,
values: ArrayList<Item> values: ArrayList<Item>,
) : RecyclerView.Adapter<VH>() { diffUtil: DiffUtil.ItemCallback<Item>
) : ListAdapter<Item, VH>(diffUtil) {
val cleanValues = MutableLiveData<MutableList<Item>>(arrayListOf()) val cleanValues = arrayListOf<Item>()
val values = MutableLiveData<MutableList<Item>>(arrayListOf()) val values = arrayListOf<Item>()
protected var inflater: LayoutInflater = LayoutInflater.from(context)
init { init {
this.values.value = values addAll(values)
} }
protected var inflater: LayoutInflater = LayoutInflater.from(context)
var itemClickListener: OnItemClickListener? = null var itemClickListener: OnItemClickListener? = null
var itemLongClickListener: OnItemLongClickListener? = null var itemLongClickListener: OnItemLongClickListener? = null
@@ -44,13 +32,13 @@ abstract class BaseAdapter<Item, VH : BaseHolder>(
itemLongClickListener = null itemLongClickListener = null
} }
open fun getItem(position: Int): Item { override fun getItem(position: Int): Item {
return values[position] return values[position]
} }
fun add(position: Int, item: Item) { fun add(position: Int, item: Item) {
values.add(item, position) values.add(position, item)
cleanValues.add(item, position) cleanValues.add(position, item)
} }
fun add(item: Item) { fun add(item: Item) {
@@ -64,8 +52,8 @@ abstract class BaseAdapter<Item, VH : BaseHolder>(
} }
fun addAll(position: Int, items: List<Item>) { fun addAll(position: Int, items: List<Item>) {
values.addAll(items, position) values.addAll(position, items)
cleanValues.addAll(items, position) cleanValues.addAll(position, items)
} }
fun removeAll(items: List<Item>) { fun removeAll(items: List<Item>) {
@@ -0,0 +1,3 @@
package com.meloda.fast.base.adapter
abstract class BaseItem
@@ -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<EmptyHeaderAdapter.Holder>() {
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
}
}
@@ -1,14 +1,15 @@
package com.meloda.fast.base package com.meloda.fast.base.adapter
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
abstract class BaseHolder(v: View) : RecyclerView.ViewHolder(v) { abstract class BaseHolder(v: View) : RecyclerView.ViewHolder(v) {
open fun bind(position: Int) { open fun bind(position: Int) {}
bind(position, mutableListOf())
}
open fun bind(position: Int, payloads: MutableList<Any>?) {} open fun bind(position: Int, payloads: MutableList<Any>?) {}
} }
abstract class BindingHolder<B : ViewBinding>(protected val binding: B) : BaseHolder(binding.root)
@@ -2,11 +2,13 @@ package com.meloda.fast.common
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteDatabase
import android.os.Handler import android.os.Handler
import android.view.inputmethod.InputMethodManager
import androidx.core.content.pm.PackageInfoCompat import androidx.core.content.pm.PackageInfoCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.meloda.fast.BuildConfig import com.meloda.fast.BuildConfig
@@ -34,6 +36,8 @@ class AppGlobal : Application() {
companion object { companion object {
lateinit var inputMethodManager: InputMethodManager
lateinit var preferences: SharedPreferences lateinit var preferences: SharedPreferences
lateinit var locale: Locale lateinit var locale: Locale
lateinit var handler: Handler lateinit var handler: Handler
@@ -82,6 +86,8 @@ class AppGlobal : Application() {
screenWidth = AndroidUtils.getDisplayWidth() screenWidth = AndroidUtils.getDisplayWidth()
screenHeight = AndroidUtils.getDisplayHeight() screenHeight = AndroidUtils.getDisplayHeight()
inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
} }
} }
@@ -2,14 +2,11 @@ package com.meloda.fast.fragment.login
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Bundle import android.os.Bundle
import android.view.Gravity
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.viewbinding.library.fragment.viewBinding import android.viewbinding.library.fragment.viewBinding
import android.webkit.CookieManager import android.webkit.CookieManager
import android.widget.LinearLayout
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
@@ -19,14 +16,15 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import coil.load import coil.load
import com.google.android.material.imageview.ShapeableImageView import coil.transform.RoundedCornersTransformation
import com.google.android.material.textfield.TextInputEditText import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.meloda.fast.R import com.meloda.fast.R
import com.meloda.fast.base.BaseVMFragment import com.meloda.fast.base.BaseVMFragment
import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VKEvent import com.meloda.fast.base.viewmodel.VKEvent
import com.meloda.fast.databinding.DialogCaptchaBinding
import com.meloda.fast.databinding.FragmentLoginBinding import com.meloda.fast.databinding.FragmentLoginBinding
import com.meloda.fast.fragment.main.MainFragment import com.meloda.fast.fragment.main.MainFragment
import com.meloda.fast.util.KeyboardUtils import com.meloda.fast.util.KeyboardUtils
@@ -36,7 +34,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
import kotlin.concurrent.schedule import kotlin.concurrent.schedule
import kotlin.math.roundToInt
@AndroidEntryPoint @AndroidEntryPoint
class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) { class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
@@ -48,6 +45,7 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
private var lastPassword: String = "" private var lastPassword: String = ""
private var errorTimer: Timer? = null private var errorTimer: Timer? = null
private var captchaInputLayout: TextInputLayout? = null
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
@@ -56,20 +54,28 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
prepareViews() prepareViews()
binding.loginInput.clearFocus()
setFragmentResultListener("validation") { _, bundle -> setFragmentResultListener("validation") { _, bundle ->
lifecycleScope.launch { viewModel.getValidatedData(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) { override fun onEvent(event: VKEvent) {
super.onEvent(event) super.onEvent(event)
when (event) { when (event) {
is ShowError -> showErrorSnackbar(event.errorDescription)
is ShowCaptchaDialog -> showCaptchaDialog(event.captchaImage, event.captchaSid) is ShowCaptchaDialog -> showCaptchaDialog(event.captchaImage, event.captchaSid)
is GoToValidationEvent -> goToValidation(event.redirectUrl) is GoToValidationEvent -> goToValidation(event.redirectUrl)
is GoToMainEvent -> goToMain(event.haveAuthorized) is GoToMainEvent -> goToMain(event.haveAuthorized)
StartProgressEvent -> onProgressStarted() StartProgressEvent -> onProgressStarted()
StopProgressEvent -> onProgressEnded() StopProgressEvent -> onProgressStopped()
} }
} }
@@ -80,7 +86,7 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
binding.progress.isVisible = true binding.progress.isVisible = true
} }
private fun onProgressEnded() { private fun onProgressStopped() {
binding.loginContainer.isVisible = true binding.loginContainer.isVisible = true
binding.passwordContainer.isVisible = true binding.passwordContainer.isVisible = true
binding.auth.isVisible = true binding.auth.isVisible = true
@@ -150,7 +156,7 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
if (!validateInputData(loginString, passwordString)) return@setOnClickListener if (!validateInputData(loginString, passwordString)) return@setOnClickListener
KeyboardUtils.hideKeyboardFrom(it) KeyboardUtils.hideKeyboardFrom(requireView().findFocus())
lifecycleScope.launch { lifecycleScope.launch {
viewModel.login( viewModel.login(
@@ -162,19 +168,29 @@ class LoginFragment : BaseVMFragment<LoginVM>(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 var isValidated = true
if (loginString.isEmpty()) { if (loginString?.isEmpty() == true) {
isValidated = false isValidated = false
setError("Input login", binding.loginLayout) setError("Input login", binding.loginLayout)
} }
if (passwordString.isEmpty()) { if (passwordString?.isEmpty() == true) {
isValidated = false isValidated = false
setError("Input password", binding.passwordLayout) setError("Input password", binding.passwordLayout)
} }
if (captchaCode?.isEmpty() == true && captchaInputLayout != null) {
isValidated = false
setError("Input code", captchaInputLayout!!)
}
return isValidated return isValidated
} }
@@ -198,59 +214,60 @@ class LoginFragment : BaseVMFragment<LoginVM>(R.layout.fragment_login) {
private fun clearErrors() { private fun clearErrors() {
binding.loginLayout.error = "" binding.loginLayout.error = ""
binding.passwordLayout.error = "" binding.passwordLayout.error = ""
captchaInputLayout?.error = ""
} }
// TODO: 7/10/2021 extract layout to resources
private fun showCaptchaDialog(captchaImage: String, captchaSid: String) { 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() captchaBinding.image.load(captchaImage) {
val height = metrics.heightPixels / 7 crossfade(100)
transformations(RoundedCornersTransformation(4f))
val image = ShapeableImageView(requireContext()).also {
it.layoutParams = ViewGroup.LayoutParams(width, height)
} }
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()) 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 captchaBinding.ok.setOnClickListener {
layout.gravity = Gravity.CENTER val captchaCode = captchaBinding.captchaInput.text.toString().trim()
layout.addView(image)
layout.addView(captchaCodeEditText)
builder.setView(layout) if (!validateInputData(
builder.setNegativeButton(android.R.string.cancel, null) loginString = null,
builder.setPositiveButton(android.R.string.ok) { _, _ -> passwordString = null,
val captchaCode = captchaCodeEditText.text.toString().trim() captchaCode = captchaCode
)
) return@setOnClickListener
dialog.dismiss()
lifecycleScope.launch { lifecycleScope.launch {
viewModel.login( viewModel.login(
binding.webView, webView = binding.webView,
lastEmail, email = lastEmail,
lastPassword, password = lastPassword,
"&captcha_sid=$captchaSid&captcha_key=$captchaCode" captchaSid = captchaSid,
captchaKey = captchaCode
) )
} }
} }
captchaBinding.cancel.setOnClickListener { dialog.dismiss() }
}
builder.setTitle(R.string.input_captcha) private fun showErrorSnackbar(errorDescription: String) {
builder.show() 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) { private fun goToValidation(redirectUrl: String) {
@@ -1,6 +1,7 @@
package com.meloda.fast.fragment.login package com.meloda.fast.fragment.login
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
@@ -19,20 +20,21 @@ import org.jsoup.Jsoup
class LoginVM : BaseVM() { class LoginVM : BaseVM() {
private var isWebViewPrepared = true private var isWebViewPrepared = false
suspend fun login( suspend fun login(
webView: WebView, webView: WebView,
email: String, email: String,
password: String, password: String,
captcha: String = "" captchaSid: String? = null,
captchaKey: String? = null
) { ) {
sendEvent(StartProgressEvent) sendEvent(StartProgressEvent)
val urlToGo = VKAuth.getDirectAuthUrl(email, password, captcha) val urlToGo = VKAuth.getDirectAuthUrl(email, password, captchaSid, captchaKey)
if (isWebViewPrepared) { if (!isWebViewPrepared) {
isWebViewPrepared = false isWebViewPrepared = true
webView.addJavascriptInterface(WebViewHandlerInterface(), "HtmlHandler") webView.addJavascriptInterface(WebViewHandlerInterface(), "HtmlHandler")
@@ -52,11 +54,14 @@ class LoginVM : BaseVM() {
@Suppress("MoveVariableDeclarationIntoWhen") @Suppress("MoveVariableDeclarationIntoWhen")
private fun checkResponse(response: JSONObject) { private fun checkResponse(response: JSONObject) {
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
delay(1500)
sendEvent(StopProgressEvent)
if (response.has("error")) { if (response.has("error")) {
sendEvent(StopProgressEvent)
val errorString = response.optString("error") 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) { when (errorString) {
"need_validation" -> { "need_validation" -> {
@@ -68,10 +73,18 @@ class LoginVM : BaseVM() {
val captchaImage = response.optString("captcha_img") val captchaImage = response.optString("captcha_img")
val captchaSid = response.optString("captcha_sid") val captchaSid = response.optString("captcha_sid")
Log.d("CAPTCHA", "captchaImage: $captchaImage")
tasksEventChannel.send(ShowCaptchaDialog(captchaImage, captchaSid)) tasksEventChannel.send(ShowCaptchaDialog(captchaImage, captchaSid))
} }
else -> {
tasksEventChannel.send(ShowError(errorDescription))
}
} }
} else { } else {
delay(1500)
sendEvent(StopProgressEvent)
val userId = response.optInt("user_id", -1) val userId = response.optInt("user_id", -1)
val accessToken = response.optString("access_token") 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 ShowCaptchaDialog(val captchaImage: String, val captchaSid: String) : VKEvent()
data class GoToValidationEvent(val redirectUrl: String) : VKEvent() data class GoToValidationEvent(val redirectUrl: String) : VKEvent()
data class GoToMainEvent(val haveAuthorized: Boolean = true) : VKEvent() data class GoToMainEvent(val haveAuthorized: Boolean = true) : VKEvent()
@@ -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<VKConversation>) :
BaseAdapter<VKConversation, ConversationsAdapter.ItemHolder>(
context, values, COMPARATOR
) {
companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<VKConversation>() {
override fun areItemsTheSame(
oldItem: VKConversation,
newItem: VKConversation
) = false
override fun areContentsTheSame(
oldItem: VKConversation,
newItem: VKConversation
) = false
}
}
inner class ItemHolder(binding: ItemConversationBinding) :
BindingHolder<ItemConversationBinding>(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))
}
@@ -3,19 +3,81 @@ package com.meloda.fast.fragment.messages
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import android.viewbinding.library.fragment.viewBinding import android.viewbinding.library.fragment.viewBinding
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import com.meloda.fast.R 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.databinding.FragmentConversationsBinding
import com.meloda.fast.util.AndroidUtils
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlin.math.roundToInt
@AndroidEntryPoint @AndroidEntryPoint
class ConversationsFragment : BaseFragment(R.layout.fragment_conversations) { class ConversationsFragment : BaseVMFragment<ConversationsVM>(R.layout.fragment_conversations) {
override val viewModel: ConversationsVM by viewModels()
private val binding: FragmentConversationsBinding by viewBinding() private val binding: FragmentConversationsBinding by viewBinding()
private lateinit var adapter: ConversationsAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) 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 { }
}
} }
} }
@@ -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) {
}
}
@@ -1,16 +1,16 @@
package com.meloda.fast.util package com.meloda.fast.util
import android.view.View import android.view.View
import com.meloda.fast.common.AppGlobal
//TODO
object KeyboardUtils { object KeyboardUtils {
fun hideKeyboardFrom(view: View) { fun hideKeyboardFrom(focusedView: View?) {
// AppGlobal.inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0) AppGlobal.inputMethodManager.hideSoftInputFromWindow(focusedView?.windowToken, 0)
} }
fun showKeyboard(focusedView: View) { fun showKeyboard(viewToFocus: View) {
// AppGlobal.inputMethodManager.showSoftInput(focusedView, 0) AppGlobal.inputMethodManager.showSoftInput(viewToFocus, 0)
} }
} }
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M12,12H19C18.47,16.11 15.72,19.78 12,20.92V12H5V6.3L12,3.19M12,1L3,5V11C3,16.55 6.84,21.73 12,23C17.16,21.73 21,16.55 21,11V5L12,1Z" />
</vector>
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="@dimen/activity_horizontal_margin">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/image"
android:layout_width="match_parent"
android:layout_height="50dp"
tools:src="@tools:sample/backgrounds/scenic" />
<LinearLayout
android:id="@+id/captchaContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_vertical_margin"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/captchaImage"
style="@style/AppTheme.Login.EditText.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:src="@drawable/ic_security"
app:tint="?colorAccent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/captchaLayout"
style="@style/Widget.TextInputLayout.NoError.Dense"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/captchaInput"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/captcha_hint"
android:imeOptions="actionGo"
android:lines="1"
android:maxLines="1" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<com.google.android.material.button.MaterialButton
android:id="@+id/cancel"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:layout_weight="1"
android:text="@android:string/cancel"
app:elevation="0dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/ok"
style="@style/Widget.MaterialComponents.Button.UnelevatedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@android:string/ok"
app:elevation="0dp" />
</LinearLayout>
</LinearLayout>
</layout>
@@ -1,45 +1,47 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<TextView <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:layout_width="wrap_content" android:id="@+id/refreshLayout"
android:layout_height="wrap_content"
android:text="conversations" />
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/item_conversation" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp" android:layout_marginBottom="8dp"
android:background="@android:color/transparent" android:background="@drawable/toolbar_background"
app:elevation="0dp"> android:elevation="3dp"
app:title="@string/conversations"
<com.google.android.material.appbar.CollapsingToolbarLayout app:titleCentered="true" />
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?actionBarSize"
app:titleCentered="true" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/recycler_view" />
<ProgressBar <ProgressBar
android:id="@+id/progressBar" android:id="@+id/progressBar"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:visibility="gone" /> android:visibility="gone"
tools:visibility="visible" />
<include <include
layout="@layout/no_items_view" layout="@layout/no_items_view"
+6 -6
View File
@@ -73,7 +73,8 @@
android:id="@+id/loginLayout" android:id="@+id/loginLayout"
style="@style/Widget.TextInputLayout.NoError.Dense" style="@style/Widget.TextInputLayout.NoError.Dense"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
app:boxStrokeErrorColor="@android:color/transparent">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:id="@+id/loginInput" android:id="@+id/loginInput"
@@ -81,12 +82,10 @@
android:layout_height="48dp" android:layout_height="48dp"
android:hint="@string/login_hint" android:hint="@string/login_hint"
android:imeOptions="actionGo" android:imeOptions="actionGo"
android:inputType="textEmailAddress" android:inputType="textEmailAddress" />
android:textCursorDrawable="@null" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
</LinearLayout> </LinearLayout>
<LinearLayout <LinearLayout
@@ -110,6 +109,7 @@
style="@style/Widget.TextInputLayout.NoError.Dense" style="@style/Widget.TextInputLayout.NoError.Dense"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:boxStrokeErrorColor="@android:color/transparent"
app:passwordToggleEnabled="true" app:passwordToggleEnabled="true"
app:passwordToggleTint="?colorAccent"> app:passwordToggleTint="?colorAccent">
@@ -117,9 +117,9 @@
android:id="@+id/passwordInput" android:id="@+id/passwordInput"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="48dp"
android:imeOptions="actionGo"
android:hint="@string/password_login_hint" android:hint="@string/password_login_hint"
android:inputType="textPassword" android:inputType="textPassword" />
android:textCursorDrawable="@null" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
+16 -16
View File
@@ -2,7 +2,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/conversationRoot" android:id="@+id/root"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?selectableItemBackground" android:background="?selectableItemBackground"
@@ -12,7 +12,7 @@
android:paddingBottom="2dp"> android:paddingBottom="2dp">
<LinearLayout <LinearLayout
android:id="@+id/conversationContainer" android:id="@+id/container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_margin="6dp" android:layout_margin="6dp"
@@ -27,15 +27,15 @@
android:layout_weight="0"> android:layout_weight="0">
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/conversationAvatar" android:id="@+id/peerAvatar"
android:layout_width="56dp" android:layout_width="56dp"
android:layout_height="56dp" android:layout_height="56dp"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
tools:actualImageResource="@color/accent" app:shapeAppearanceOverlay="@style/CircleImageView.56"
tools:src="?colorAccent" /> tools:src="?colorAccent" />
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/conversationUserOnline" android:id="@+id/peerOnline"
android:layout_width="14dp" android:layout_width="14dp"
android:layout_height="14dp" android:layout_height="14dp"
android:layout_gravity="bottom|end" android:layout_gravity="bottom|end"
@@ -59,7 +59,7 @@
android:orientation="horizontal"> android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/conversationType" android:id="@+id/type"
android:layout_width="20dp" android:layout_width="20dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
@@ -70,7 +70,7 @@
tools:src="@drawable/ic_dialog_type_conversation" /> tools:src="@drawable/ic_dialog_type_conversation" />
<TextView <TextView
android:id="@+id/conversationTitle" android:id="@+id/title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp" android:layout_marginStart="8dp"
@@ -93,17 +93,17 @@
android:orientation="horizontal"> android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/conversationUserAvatar" android:id="@+id/fromAvatar"
android:layout_width="20dp" android:layout_width="24dp"
android:layout_height="20dp" android:layout_height="24dp"
android:layout_gravity="center_vertical" android:layout_gravity="center_vertical"
android:layout_marginStart="6dp" android:layout_marginStart="6dp"
tools:actualImageResource="@color/accent" app:shapeAppearanceOverlay="@style/CircleImageView.24"
tools:src="?colorAccent" tools:src="?colorAccent"
tools:visibility="visible" /> tools:visibility="visible" />
<androidx.appcompat.widget.AppCompatImageView <androidx.appcompat.widget.AppCompatImageView
android:id="@+id/conversationTextAttachment" android:id="@+id/textAttachment"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
@@ -114,7 +114,7 @@
tools:visibility="visible" /> tools:visibility="visible" />
<TextView <TextView
android:id="@+id/conversationText" android:id="@+id/text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:layout_marginStart="4dp"
@@ -138,7 +138,7 @@
android:paddingEnd="6dp"> android:paddingEnd="6dp">
<com.google.android.material.imageview.ShapeableImageView <com.google.android.material.imageview.ShapeableImageView
android:id="@+id/conversationOut" android:id="@+id/out"
android:layout_width="10dp" android:layout_width="10dp"
android:layout_height="10dp" android:layout_height="10dp"
android:layout_gravity="bottom" android:layout_gravity="bottom"
@@ -151,7 +151,7 @@
</LinearLayout> </LinearLayout>
<TextView <TextView
android:id="@+id/conversationDate" android:id="@+id/date"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end|top" android:layout_gravity="end|top"
@@ -164,7 +164,7 @@
tools:text="now" /> tools:text="now" />
<TextView <TextView
android:id="@+id/conversationCounter" android:id="@+id/counter"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="primary">#ffffff</color> <color name="primary">@color/accent</color>
<color name="primaryDark">#ffffff</color> <color name="primaryDark">#ffffff</color>
<color name="accent">#4284F4</color> <color name="accent">#4284F4</color>
<color name="navigationBar">#ffffff</color> <color name="navigationBar">#ffffff</color>
+3 -1
View File
@@ -150,9 +150,11 @@
<string name="password_login_hint">Password</string> <string name="password_login_hint">Password</string>
<string name="email_login_hint">E-mail or phone number</string> <string name="email_login_hint">E-mail or phone number</string>
<string name="log_in">Log in</string> <string name="log_in">Log in</string>
<string name="captcha_hint">Captcha</string> <string name="captcha_hint">Captcha code</string>
<string name="input_captcha">Input code from picture</string> <string name="input_captcha">Input code from picture</string>
<string name="login_hint">Login</string> <string name="login_hint">Login</string>
<string name="conversations">Conversations</string>
</resources> </resources>
+14
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="AppTheme.ActivityAnimation" parent="@android:style/Animation.Activity"> <style name="AppTheme.ActivityAnimation" parent="@android:style/Animation.Activity">
<item name="android:activityOpenEnterAnimation">@anim/activity_open_enter</item> <item name="android:activityOpenEnterAnimation">@anim/activity_open_enter</item>
<item name="android:activityOpenExitAnimation">@anim/activity_open_exit</item> <item name="android:activityOpenExitAnimation">@anim/activity_open_exit</item>
@@ -11,4 +12,17 @@
<item name="android:windowEnterAnimation">@anim/slide_up</item> <item name="android:windowEnterAnimation">@anim/slide_up</item>
<item name="android:windowExitAnimation">@anim/slide_down</item> <item name="android:windowExitAnimation">@anim/slide_down</item>
</style> </style>
<style name="CircleImageView">
<item name="cornerFamily">rounded</item>
</style>
<style name="CircleImageView.56">
<item name="cornerRadius">56dp</item>
</style>
<style name="CircleImageView.24">
<item name="cornerRadius">24dp</item>
</style>
</resources> </resources>
+6 -11
View File
@@ -27,6 +27,7 @@
<item name="android:navigationBarDividerColor" tools:targetApi="o_mr1"> <item name="android:navigationBarDividerColor" tools:targetApi="o_mr1">
@android:color/transparent @android:color/transparent
</item> </item>
</style> </style>
<style name="AppTheme.Toolbar" parent="Widget.MaterialComponents.Toolbar.PrimarySurface"> <style name="AppTheme.Toolbar" parent="Widget.MaterialComponents.Toolbar.PrimarySurface">
@@ -37,21 +38,15 @@
</style> </style>
<style name="Toolbar.Title" parent="TextAppearance.MaterialComponents.Body1"> <style name="Toolbar.Title" parent="TextAppearance.MaterialComponents.Body1">
<item name="android:textSize">24sp</item> <item name="android:textSize">20sp</item>
<item name="android:fontFamily">@font/tt_commons_medium</item> <item name="android:textColor">@color/accent</item>
<item name="android:fontFamily">@font/google_sans_medium</item>
</style> </style>
<style name="AppTheme.Dialog" parent="Theme.MaterialComponents.DayNight.Dialog.Alert" /> <style name="AppTheme.Dialog" parent="Theme.MaterialComponents.DayNight.Dialog.Alert" />
<style name="AppTheme.Login.EditText" parent=""> <style name="AppTheme.Login.EditText" parent="Widget.Design.TextInputEditText">
<item name="android:layout_height">52dp</item> <item name="android:layout_height">48dp</item>
<item name="android:background">@drawable/edittext_filled_background</item>
<item name="android:paddingStart">16dp</item>
<item name="android:paddingEnd">16dp</item>
<item name="android:layout_marginEnd">16dp</item>
<item name="android:textSize">14sp</item>
<item name="android:textColor">?android:textColorPrimary</item>
<item name="android:textColorHint">?textColorSecondary</item>
<item name="fontFamily">@font/google_sans_regular</item> <item name="fontFamily">@font/google_sans_regular</item>
<item name="android:singleLine">true</item> <item name="android:singleLine">true</item>
<item name="android:maxLines">1</item> <item name="android:maxLines">1</item>
+1 -1
View File
@@ -6,7 +6,7 @@ buildscript {
} }
dependencies { dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20")
classpath("com.android.tools.build:gradle:4.2.2") classpath("com.android.tools.build:gradle:7.0.0")
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5") classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.3.5")
classpath("com.google.dagger:hilt-android-gradle-plugin:2.37") classpath("com.google.dagger:hilt-android-gradle-plugin:2.37")
+1 -1
View File
@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip