diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f29f3a69..6e8f6777 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,9 +7,9 @@ plugins { id("com.android.application") id("kotlin-android") id("kotlin-kapt") + id("kotlin-parcelize") id("androidx.navigation.safeargs.kotlin") id("dagger.hilt.android.plugin") - id("kotlin-parcelize") } android { @@ -38,6 +38,9 @@ android { getByName("release") { isMinifyEnabled = false + buildConfigField("String", "vkLogin", login) + buildConfigField("String", "vkPassword", password) + proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" @@ -68,7 +71,7 @@ android { kapt { correctErrorTypes = true - //use this shit if you don't want to have hilt errors + //use this shit if you don't want have hilt errors javacOptions { option("-Adagger.hilt.android.internal.disableAndroidSuperclassValidation=true") } @@ -83,6 +86,8 @@ dependencies { implementation("androidx.datastore:datastore-preferences:1.0.0") + implementation("androidx.paging:paging-runtime-ktx:3.0.1") + implementation("androidx.appcompat:appcompat:1.4.0-alpha03") implementation("com.google.android.material:material:1.5.0-alpha03") implementation("androidx.core:core-ktx:1.7.0-beta01") diff --git a/app/src/main/kotlin/com/meloda/fast/api/UserConfig.kt b/app/src/main/kotlin/com/meloda/fast/api/UserConfig.kt index 63302e92..402862de 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/UserConfig.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/UserConfig.kt @@ -6,6 +6,7 @@ import com.meloda.fast.common.AppGlobal object UserConfig { + private const val FAST_TOKEN = "fast_token" private const val TOKEN = "token" private const val USER_ID = "user_id" @@ -25,8 +26,16 @@ object UserConfig { AppGlobal.preferences.edit().putString(TOKEN, value).apply() } + var fastToken: String = "" + get() = AppGlobal.preferences.getString(FAST_TOKEN, "") ?: "" + set(value) { + field = value + AppGlobal.preferences.edit().putString(FAST_TOKEN, value).apply() + } + fun clear() { accessToken = "" + fastToken = "" userId = -1 } diff --git a/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt b/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt index cc2567ba..ebfbd0bd 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt @@ -7,6 +7,8 @@ object VKConstants { const val USER_FIELDS = "photo_50,photo_100,photo_200,status,screen_name,online,online_mobile,last_seen,verified,sex,online_info" + const val ALL_FIELDS = "$USER_FIELDS,$GROUP_FIELDS" + const val API_VERSION = "5.132" const val VK_APP_ID = "2274003" const val VK_SECRET = "hHbZxrka2uZ6jB1inYsH" diff --git a/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt b/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt index 3f9adcab..d2b4d302 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt @@ -42,6 +42,12 @@ object VkUtils { return forwards } + fun parseReplyMessage(baseReplyMessage: BaseVkMessage?): VkMessage? { + if (baseReplyMessage == null) return null + + return baseReplyMessage.asVkMessage() + } + fun parseAttachments(baseAttachments: List?): List? { if (baseAttachments.isNullOrEmpty()) return null diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt index b7e38ecb..cd0beaca 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt @@ -1,9 +1,12 @@ package com.meloda.fast.api.model -import android.os.Parcelable +import androidx.lifecycle.MutableLiveData import androidx.room.Entity +import androidx.room.Ignore import androidx.room.PrimaryKey import com.meloda.fast.api.model.attachments.VkAttachment +import com.meloda.fast.base.adapter.SelectableItem +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Entity(tableName = "messages") @@ -24,9 +27,21 @@ data class VkMessage( val actionMessage: String? = null, val geoType: String? = null, val important: Boolean = false, + var forwards: List? = null, - var attachments: List? = null -) : Parcelable { + var attachments: List? = null, + +// @Embedded(prefix = "replyMessage_") + var replyMessage: VkMessage? = null +) : SelectableItem() { + + @Ignore + @IgnoredOnParcel + val user = MutableLiveData() + + @Ignore + @IgnoredOnParcel + val group = MutableLiveData() fun isPeerChat() = peerId > 2_000_000_000 diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt index 2531d5a8..6c3d9d56 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt @@ -1,8 +1,10 @@ package com.meloda.fast.api.model.attachments +import androidx.room.Ignore import com.meloda.fast.api.model.base.attachments.BaseVkPhoto import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize +import java.util.* @Parcelize data class VkPhoto( @@ -17,13 +19,58 @@ data class VkPhoto( val userId: Int? ) : VkAttachment() { + @Ignore + @IgnoredOnParcel + private val sizesChars = Stack() + + init { + sizesChars.push('s') + sizesChars.push('m') + sizesChars.push('x') + sizesChars.push('o') + sizesChars.push('p') + sizesChars.push('q') + sizesChars.push('r') + sizesChars.push('y') + sizesChars.push('z') + sizesChars.push('w') + } + @IgnoredOnParcel val className: String = this::class.java.name - fun sizeOfType(type: Char): BaseVkPhoto.Size? { + fun getMaxSize(): BaseVkPhoto.Size? { + return getSizeOrSmaller(sizesChars.peek()) + } + + fun getSizeOrNull(type: Char): BaseVkPhoto.Size? { for (size in sizes) { - if (size.type == type.toString()) + if (size.type == type.toString()) return size + } + + return null + } + + fun getSizeOrSmaller(type: Char): BaseVkPhoto.Size? { + val photoStack = sizesChars.clone() as Stack + + val sizeIndex = photoStack.search(type) + + if (sizeIndex == -1) return null + + for (i in 0 until sizeIndex) { + photoStack.pop() + } + + for (i in 0 until photoStack.size) { + val size = getSizeOrNull(photoStack.peek()) + + if (size == null) { + photoStack.pop() + continue + } else { return size + } } return null diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt index 8d2a57cb..f431fbff 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkMessage.kt @@ -23,7 +23,8 @@ data class BaseVkMessage( val payload: String, val geo: Geo?, val action: Action?, - val ttl: Int + val ttl: Int, + val reply_message: BaseVkMessage? ) : Parcelable { fun asVkMessage() = VkMessage( @@ -44,6 +45,7 @@ data class BaseVkMessage( ).also { it.attachments = VkUtils.parseAttachments(attachments) it.forwards = VkUtils.parseForwards(fwd_messages) + it.replyMessage = VkUtils.parseReplyMessage(reply_message) } @Parcelize diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt b/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt index 8649e89e..b6f96691 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/AuthInterceptor.kt @@ -17,6 +17,7 @@ class AuthInterceptor : Interceptor { builder.addQueryParameter("access_token", URLEncoder.encode(it, "utf-8")) } + // TODO: 9/29/2021 crash on timeout return chain.proceed(chain.request().newBuilder().apply { url(builder.build()) }.build()) } diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt index e77ba993..74870bc6 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/adapter/BaseAdapter.kt @@ -114,7 +114,7 @@ abstract class BaseAdapter( holder.bind(position) } - protected fun initListeners(itemView: View, position: Int) { + protected open fun initListeners(itemView: View, position: Int) { if (itemView is AdapterView<*>) return itemView.setOnClickListener { itemClickListener.invoke(position) } diff --git a/app/src/main/kotlin/com/meloda/fast/base/adapter/Items.kt b/app/src/main/kotlin/com/meloda/fast/base/adapter/Items.kt new file mode 100644 index 00000000..cfa64546 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/base/adapter/Items.kt @@ -0,0 +1,15 @@ +package com.meloda.fast.base.adapter + +import android.os.Parcelable +import androidx.room.Ignore +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +open class SelectableItem : Parcelable { + + @Ignore + @IgnoredOnParcel + var isSelected: Boolean = false + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt b/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt index eb0a7ea4..fb47317e 100644 --- a/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt +++ b/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt @@ -19,7 +19,7 @@ import com.meloda.fast.database.dao.UsersDao VkUser::class, VkGroup::class ], - version = 24, + version = 25, exportSchema = false, ) @TypeConverters(Converters::class) diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/Extensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/Extensions.kt index 9c308106..05ff43e3 100644 --- a/app/src/main/kotlin/com/meloda/fast/extensions/Extensions.kt +++ b/app/src/main/kotlin/com/meloda/fast/extensions/Extensions.kt @@ -1,6 +1,8 @@ package com.meloda.fast.extensions import android.graphics.* +import android.view.View +import androidx.core.view.isVisible import kotlin.math.min fun Bitmap.borderedCircularBitmap( @@ -70,4 +72,6 @@ fun Bitmap.borderedCircularBitmap( diameter, // width diameter // height ) -} \ No newline at end of file +} + +val View.isNotVisible get() = !isVisible \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt index a90f98f8..63bb37a3 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt @@ -1,25 +1,31 @@ package com.meloda.fast.screens.conversations +import android.content.Intent import android.os.Bundle +import android.view.Gravity import android.view.View import android.viewbinding.library.fragment.viewBinding +import androidx.appcompat.widget.PopupMenu import androidx.core.content.ContextCompat import androidx.core.os.bundleOf import androidx.core.view.isVisible +import androidx.core.view.updatePadding import androidx.datastore.preferences.core.edit import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import coil.load import com.google.android.material.appbar.AppBarLayout -import com.google.android.material.snackbar.Snackbar +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.meloda.fast.R +import com.meloda.fast.activity.MainActivity import com.meloda.fast.api.UserConfig import com.meloda.fast.api.model.VkConversation import com.meloda.fast.base.BaseViewModelFragment import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent import com.meloda.fast.base.viewmodel.VKEvent +import com.meloda.fast.common.AppGlobal import com.meloda.fast.common.AppSettings import com.meloda.fast.common.dataStore import com.meloda.fast.databinding.FragmentConversationsBinding @@ -29,6 +35,7 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlin.math.abs +import kotlin.math.roundToInt @AndroidEntryPoint class ConversationsFragment : @@ -53,6 +60,24 @@ class ConversationsFragment : } } + private val avatarPopupMenu: PopupMenu + get() = + PopupMenu( + requireContext(), + binding.avatar, + Gravity.BOTTOM + ).apply { + menu.add(getString(R.string.log_out)) + setOnMenuItemClickListener { item -> + if (item.title == getString(R.string.log_out)) { + showLogOutDialog() + return@setOnMenuItemClickListener true + } + + false + } + } + private var isPaused = false private var isExpanded = true @@ -74,11 +99,7 @@ class ConversationsFragment : }.collect { } } - binding.createChat.setOnClickListener { - Snackbar.make(it, "Test snackbar", Snackbar.LENGTH_SHORT) - .setAction("Action") {} - .show() - } + binding.createChat.setOnClickListener {} UserConfig.vkUser.observe(viewLifecycleOwner) { it?.let { user -> binding.avatar.load(user.photo200) { crossfade(100) } } @@ -87,28 +108,49 @@ class ConversationsFragment : binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset -> if (isPaused) return@OnOffsetChangedListener - if (verticalOffset <= -100) { - binding.avatarContainer.alpha = 0f - return@OnOffsetChangedListener - } - val alpha = 1 - abs(verticalOffset * 0.01).toFloat() +// if (verticalOffset <= -100) { +// binding.avatarContainer.alpha = 0f +// return@OnOffsetChangedListener +// } - binding.avatarContainer.alpha = alpha + // from 0 to -294 + // from 0 to 29 + + // if -147 + // 30 - value + + var value = 30 - (abs(verticalOffset) * 0.1).roundToInt() + + val bottomPadding = 0 +// if (verticalOffset > -150) AndroidUtils.px(30).roundToInt() +// else (30 + abs(verticalOffset) * 0.1).roundToInt() + + val endPadding = 0 +// if (verticalOffset > 30) 30 +// else (abs(verticalOffset) * 0.1).roundToInt() + + binding.avatarContainer.updatePadding( + bottom = value, + right = endPadding + ) + + + println("Fast::ConversationsFragment::onOffset verticalOffset = $verticalOffset; bottomPadding = $value; endPadding = $endPadding") + + +// binding.avatarContainer.alpha = alpha }) - if (isPaused) { - isPaused = false - return - } - binding.toolbar.overflowIcon = ContextCompat.getDrawable(requireContext(), R.drawable.test) - viewModel.loadProfileUser() - viewModel.loadConversations() binding.avatar.setOnClickListener { - lifecycleScope.launchWhenResumed { + avatarPopupMenu.show() + } + + binding.avatar.setOnLongClickListener { + lifecycleScope.launch { requireContext().dataStore.edit { settings -> val isMultilineEnabled = settings[AppSettings.keyIsMultilineEnabled] ?: true settings[AppSettings.keyIsMultilineEnabled] = !isMultilineEnabled @@ -117,7 +159,30 @@ class ConversationsFragment : adapter.notifyItemRangeChanged(0, adapter.itemCount) } } + true } + + if (isPaused) { + isPaused = false + return + } + + viewModel.loadProfileUser() + viewModel.loadConversations() + } + + private fun showLogOutDialog() { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.confirm) + .setMessage(R.string.log_out_confirm) + .setPositiveButton(R.string.yes) { _, _ -> + UserConfig.clear() + AppGlobal.appDatabase.clearAllTables() + requireActivity().finishAffinity() + requireActivity().startActivity(Intent(requireContext(), MainActivity::class.java)) + } + .setNegativeButton(R.string.no, null) + .show() } override fun onEvent(event: VKEvent) { diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt index d4d9209b..9e83ebd7 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt @@ -3,13 +3,13 @@ package com.meloda.fast.screens.conversations import androidx.lifecycle.viewModelScope import com.meloda.fast.api.UserConfig import com.meloda.fast.api.VKConstants -import com.meloda.fast.api.network.datasource.ConversationsDataSource -import com.meloda.fast.api.network.datasource.UsersDataSource import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.request.ConversationsGetRequest import com.meloda.fast.api.model.request.UsersGetRequest +import com.meloda.fast.api.network.datasource.ConversationsDataSource +import com.meloda.fast.api.network.datasource.UsersDataSource import com.meloda.fast.base.viewmodel.BaseViewModel import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent @@ -26,13 +26,15 @@ class ConversationsViewModel @Inject constructor( private val usersDataSource: UsersDataSource ) : BaseViewModel() { - fun loadConversations() = viewModelScope.launch(Dispatchers.Default) { + fun loadConversations( + offset: Int? = null + ) = viewModelScope.launch(Dispatchers.Default) { makeJob({ dataSource.getAllChats( ConversationsGetRequest( count = 30, -// offset = 177, extended = true, + offset = offset, fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}" ) ) @@ -52,6 +54,7 @@ class ConversationsViewModel @Inject constructor( sendEvent( ConversationsLoaded( count = response.count, + offset = offset, unreadCount = response.unreadCount ?: 0, conversations = response.items.map { items -> items.conversation.asVkConversation( @@ -73,9 +76,7 @@ class ConversationsViewModel @Inject constructor( } fun loadProfileUser() = viewModelScope.launch { - makeJob({ - usersDataSource.getById(UsersGetRequest(fields = "online,photo_200")) - }, + makeJob({ usersDataSource.getById(UsersGetRequest(fields = VKConstants.USER_FIELDS)) }, onAnswer = { it.response?.let { r -> val users = r.map { u -> u.asVkUser() } @@ -89,6 +90,7 @@ class ConversationsViewModel @Inject constructor( data class ConversationsLoaded( val count: Int, + val offset: Int?, val unreadCount: Int?, val conversations: List, val profiles: HashMap, diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt index 1f27553b..f96a9bcc 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt @@ -1,11 +1,17 @@ package com.meloda.fast.screens.login +import android.annotation.SuppressLint +import android.graphics.Bitmap import android.graphics.Typeface import android.os.Bundle +import android.util.Log import android.view.KeyEvent import android.view.View import android.view.inputmethod.EditorInfo import android.viewbinding.library.fragment.viewBinding +import android.webkit.CookieManager +import android.webkit.WebView +import android.webkit.WebViewClient import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.view.isVisible @@ -19,6 +25,8 @@ import com.google.android.material.snackbar.Snackbar import com.google.android.material.textfield.TextInputLayout import com.meloda.fast.BuildConfig import com.meloda.fast.R +import com.meloda.fast.api.UserConfig +import com.meloda.fast.api.VKConstants import com.meloda.fast.base.BaseViewModelFragment import com.meloda.fast.base.viewmodel.* import com.meloda.fast.databinding.DialogCaptchaBinding @@ -29,7 +37,10 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import org.jsoup.Jsoup +import java.net.URLEncoder import java.util.* +import java.util.regex.Pattern import kotlin.concurrent.schedule @AndroidEntryPoint @@ -89,11 +100,91 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo } private fun prepareViews() { + prepareWebView() prepareEmailEditText() preparePasswordEditText() prepareAuthButton() } + @SuppressLint("SetJavaScriptEnabled") + private fun prepareWebView() { + with(binding.webView) { + settings.javaScriptEnabled = true + settings.domStorageEnabled = true + + clearCache(true) + webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView?, url: String, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + parseAuthUrl(url) + } + + override fun onPageFinished(view: WebView, url: String) { + super.onPageFinished(view, url) + + val a = Jsoup.parse(url) + + val b = 0 + } + } + } + + CookieManager.getInstance().apply { + removeAllCookies(null) + flush() + setAcceptCookie(true) + } + } + + private fun launchWebView() { + binding.webView.loadUrl( + "https://oauth.vk.com/authorize?client_id=${UserConfig.FAST_APP_ID}&" + + "display=mobile&scope=136297695&" + + "redirect_uri=${ + URLEncoder.encode( + "https://oauth.vk.com/blank.html", + Charsets.UTF_8.toString() + ) + }&response_type=token&v=${VKConstants.API_VERSION}" + ) + } + + private fun parseAuthUrl(url: String) { + if (url.isBlank()) return + + if (url.startsWith("https://oauth.vk.com/blank.html")) { + if (url.contains("error")) { + Log.e("Fast::Login", "errorUrl: $url") + return + } + + val authData = parseRedirectUrl(url) + if (authData == null) { + Log.e("Fast::Login", "errorUrl: $url") + return + } + + val token = authData.first + + UserConfig.fastToken = token + } + } + + private fun parseRedirectUrl(url: String): Pair? { + val accessToken = extractPattern(url, "access_token=(.*?)&") ?: return null + val userId = extractPattern(url, "id=(\\d*)")?.toIntOrNull() ?: return null + + return accessToken to userId + } + + private fun extractPattern(string: String, pattern: String): String? { + val p = Pattern.compile(pattern) + val m = p.matcher(string) + return if (m.find()) { + m.group(1) + } else null + } + private fun prepareEmailEditText() { binding.loginInput.addTextChangedListener { if (!binding.loginLayout.error.isNullOrBlank()) binding.loginLayout.error = "" @@ -296,6 +387,8 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo private fun goToMain(haveAuthorized: Boolean) = lifecycleScope.launch { if (haveAuthorized) delay(500) + launchWebView() + findNavController().navigate(R.id.toMain) } diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt index 36e81833..6f3343eb 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt @@ -30,6 +30,7 @@ import java.text.SimpleDateFormat import java.util.* import kotlin.math.roundToInt +// TODO: 9/29/2021 use recyclerview for viewing attachments class AttachmentInflater constructor( private val context: Context, private val container: LinearLayoutCompat, @@ -94,12 +95,15 @@ class AttachmentInflater constructor( } private fun photo(photo: VkPhoto) { - val size = photo.sizeOfType('m') ?: return + val size = photo.getSizeOrSmaller('y') ?: return val newPhoto = ShapeableImageView(context).apply { layoutParams = LinearLayoutCompat.LayoutParams( - AndroidUtils.px(size.width).roundToInt(), - AndroidUtils.px(size.height).roundToInt() +// ViewGroup.LayoutParams.MATCH_PARENT, + size.width, + size.height +// AndroidUtils.px(size.width).roundToInt(), +// AndroidUtils.px(size.height).roundToInt() ) shapeAppearanceModel = @@ -222,14 +226,7 @@ class AttachmentInflater constructor( binding.caption.text = link.caption binding.caption.isVisible = !link.caption.isNullOrBlank() - binding.preview.shapeAppearanceModel.toBuilder() - .setAllCornerSizes(AndroidUtils.px(20)) - .build() - .let { - binding.preview.shapeAppearanceModel = it - } - - link.photo?.sizeOfType('m')?.let { + link.photo?.getMaxSize()?.let { binding.preview.load(it.url) { crossfade(150) } binding.preview.isVisible = true return @@ -245,8 +242,8 @@ class AttachmentInflater constructor( with(binding.image) { layoutParams = LinearLayoutCompat.LayoutParams( - AndroidUtils.px(180).roundToInt(), - AndroidUtils.px(180).roundToInt() + AndroidUtils.px(140).roundToInt(), + AndroidUtils.px(140).roundToInt() ) load(url) { crossfade(150) } diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt index c296733b..d8b22578 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt @@ -5,6 +5,7 @@ import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.view.View import android.view.ViewGroup +import android.widget.AdapterView import androidx.appcompat.widget.LinearLayoutCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil @@ -29,7 +30,11 @@ class MessagesHistoryAdapter constructor( val conversation: VkConversation, val profiles: HashMap = hashMapOf(), val groups: HashMap = hashMapOf() -) : BaseAdapter(context, values, COMPARATOR) { +) : BaseAdapter(context, values, COMPARATOR) { + + private var highlightTimer: Timer? = null + + var onItemClickListener: ((position: Int, view: View) -> Unit)? = null override fun getItemViewType(position: Int): Int { when { @@ -49,7 +54,7 @@ class MessagesHistoryAdapter constructor( private fun isPositionHeader(position: Int) = position == 0 private fun isPositionFooter(position: Int) = position >= actualSize - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BasicHolder { return when (viewType) { // magick numbers is great! HEADER -> Header(createEmptyView(60)) @@ -67,6 +72,13 @@ class MessagesHistoryAdapter constructor( } } + override fun initListeners(itemView: View, position: Int) { + if (itemView is AdapterView<*>) return + + itemView.setOnClickListener { onItemClickListener?.invoke(position, itemView) } + itemView.setOnLongClickListener { itemLongClickListener.invoke(position) } + } + private fun createEmptyView(size: Int) = View(context).apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -78,22 +90,22 @@ class MessagesHistoryAdapter constructor( isFocusable = false } - override fun onBindViewHolder(holder: Holder, position: Int) { + override fun onBindViewHolder(holder: BasicHolder, position: Int) { if (holder is Header || holder is Footer) return initListeners(holder.itemView, position) holder.bind(position) } - open inner class Holder(v: View = View(context)) : BaseHolder(v) + open inner class BasicHolder(v: View = View(context)) : BaseHolder(v) - inner class Header(v: View) : Holder(v) + inner class Header(v: View) : BasicHolder(v) - inner class Footer(v: View) : Holder(v) + inner class Footer(v: View) : BasicHolder(v) inner class IncomingMessage( private val binding: ItemMessageInBinding - ) : Holder(binding.root) { + ) : BasicHolder(binding.root) { override fun bind(position: Int) { val message = getItem(position) @@ -103,6 +115,9 @@ class MessagesHistoryAdapter constructor( MessagesPreparator( context = context, + + root = binding.root, + conversation = conversation, message = message, prevMessage = prevMessage, @@ -112,7 +127,6 @@ class MessagesHistoryAdapter constructor( bubble = binding.bubble, text = binding.text, spacer = binding.spacer, - time = binding.time, unread = binding.unread, attachmentContainer = binding.attachmentContainer, attachmentSpacer = binding.attachmentSpacer, @@ -125,11 +139,7 @@ class MessagesHistoryAdapter constructor( inner class OutgoingMessage( private val binding: ItemMessageOutBinding - ) : Holder(binding.root) { - - init { - binding.bubbleStroke.setOnClickListener { binding.bubble.performClick() } - } + ) : BasicHolder(binding.root) { override fun bind(position: Int) { val message = getItem(position) @@ -138,15 +148,14 @@ class MessagesHistoryAdapter constructor( MessagesPreparator( context = context, + root = binding.root, conversation = conversation, message = message, prevMessage = prevMessage, bubble = binding.bubble, - bubbleStroke = binding.bubbleStroke, text = binding.text, spacer = binding.spacer, - time = binding.time, unread = binding.unread, attachmentContainer = binding.attachmentContainer, attachmentSpacer = binding.attachmentSpacer, @@ -159,7 +168,7 @@ class MessagesHistoryAdapter constructor( inner class ServiceMessage( private val binding: ItemMessageServiceBinding - ) : Holder(binding.root) { + ) : BasicHolder(binding.root) { private val youPrefix = context.getString(R.string.you_message_prefix) @@ -198,7 +207,7 @@ class MessagesHistoryAdapter constructor( binding.photo.isVisible = true - val size = attachment.sizeOfType('m') ?: return@let + val size = attachment.getSizeOrSmaller('y') ?: return@let binding.photo.layoutParams = LinearLayoutCompat.LayoutParams( size.width, @@ -213,7 +222,7 @@ class MessagesHistoryAdapter constructor( } } - private val actualSize get() = values.size + val actualSize get() = values.size override fun getItemCount(): Int { if (actualSize == 0) return 2 diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt index 5c9be18b..631589f4 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt @@ -1,12 +1,15 @@ package com.meloda.fast.screens.messages -import android.graphics.Color +import android.content.res.ColorStateList import android.graphics.drawable.ColorDrawable import android.os.Bundle import android.text.TextUtils import android.view.View import android.viewbinding.library.fragment.viewBinding +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.core.view.setPadding import androidx.core.widget.doAfterTextChanged import androidx.fragment.app.viewModels import androidx.lifecycle.MutableLiveData @@ -16,6 +19,7 @@ import coil.load import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.meloda.fast.R import com.meloda.fast.api.UserConfig +import com.meloda.fast.api.VKConstants import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkMessage @@ -26,12 +30,14 @@ import com.meloda.fast.base.viewmodel.StopProgressEvent import com.meloda.fast.base.viewmodel.VKEvent import com.meloda.fast.databinding.FragmentMessagesHistoryBinding import com.meloda.fast.extensions.TextViewExtensions.clear +import com.meloda.fast.extensions.isNotVisible import com.meloda.fast.util.AndroidUtils import com.meloda.fast.util.TimeUtils import dagger.hilt.android.AndroidEntryPoint import java.text.SimpleDateFormat import java.util.* import kotlin.concurrent.schedule +import kotlin.math.roundToInt @AndroidEntryPoint class MessagesHistoryFragment : @@ -60,11 +66,14 @@ class MessagesHistoryFragment : private val adapter: MessagesHistoryAdapter by lazy { MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also { - it.itemClickListener = this::onItemClick + it.onItemClickListener = this::onItemClick it.itemLongClickListener = this::onItemLongClick } } + private val replyMessage = MutableLiveData() + private val isAttachmentPanelVisible = MutableLiveData(false) + private var timestampTimer: Timer? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -98,19 +107,7 @@ class MessagesHistoryFragment : binding.status.text = status ?: "..." - val avatar = when { - conversation.isChat() -> conversation.photo200 - conversation.isUser() -> user?.photo200 - conversation.isGroup() -> group?.photo200 - else -> null - } - - binding.avatar.load(avatar) { - crossfade(false) - error(ColorDrawable(Color.RED)) - } - - binding.online.isVisible = user?.online == true + prepareAvatar() prepareViews() @@ -166,8 +163,12 @@ class MessagesHistoryFragment : }) binding.message.doAfterTextChanged { - val newValue = if (it.toString().isNotBlank()) Action.SEND - else Action.RECORD + val canSend = + it.toString().isNotBlank() + + val newValue = + if (canSend) Action.SEND + else Action.RECORD if (action.value != newValue) action.value = newValue } @@ -195,12 +196,117 @@ class MessagesHistoryFragment : else -> return@observe } } + + isAttachmentPanelVisible.observe(viewLifecycleOwner) { + val layoutParams = binding.refreshLayout.layoutParams as CoordinatorLayout.LayoutParams + layoutParams.bottomMargin = + if (it) (binding.attachmentPanel.height / 1.5).roundToInt() else 0 + } + + hideAttachmentPanel(duration = 1) + + binding.avatar.setOnClickListener { + val isShown = binding.attachmentPanel.isVisible + + if (isShown) { + hideAttachmentPanel() + } else { + showAttachmentPanel() + } + } + + binding.attachmentPanel.setOnClickListener c@{ + val message = replyMessage.value ?: return@c + + val index = adapter.values.indexOf(message) + if (index == -1) return@c + + binding.recyclerView.smoothScrollToPosition(index) + } + + binding.dismissReply.setOnClickListener { + if (replyMessage.value != null) replyMessage.value = null + + hideAttachmentPanel() + } } + private fun prepareAvatar() { + val avatar = when { + conversation.ownerId == VKConstants.FAST_GROUP_ID -> null + conversation.isUser() -> user?.photo200 + conversation.isGroup() -> group?.photo200 + conversation.isChat() -> conversation.photo200 + else -> null + } + + binding.avatar.isVisible = avatar != null + + if (avatar == null) { + binding.avatarPlaceholder.isVisible = true + + if (conversation.ownerId == VKConstants.FAST_GROUP_ID) { + binding.placeholderBack.setImageDrawable( + ColorDrawable( + ContextCompat.getColor(requireContext(), R.color.a1_400) + ) + ) + binding.placeholder.imageTintList = + ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.a1_0)) + binding.placeholder.setImageResource(R.drawable.ic_fast_logo) + binding.placeholder.setPadding(18) + } else { + binding.placeholderBack.setImageDrawable( + ColorDrawable( + ContextCompat.getColor(requireContext(), R.color.n1_50) + ) + ) + binding.placeholder.imageTintList = + ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.n2_500)) + binding.placeholder.setImageResource(R.drawable.ic_account_circle_cut) + binding.placeholder.setPadding(0) + binding.avatar.setImageDrawable(null) + } + } else { + binding.avatar.load(avatar) { + crossfade(200) + target { + binding.avatarPlaceholder.isVisible = false + binding.avatar.setImageDrawable(it) + } + } + } + + binding.phantomIcon.isVisible = conversation.isPhantom + binding.online.isVisible = user?.online == true + binding.pin.isVisible = conversation.isPinned + } + + private fun showAttachmentPanel(duration: Long = 250) { + if (isAttachmentPanelVisible.value == false) isAttachmentPanelVisible.value = true + + binding.attachmentPanel.animate() + .translationY(0f) + .alpha(1f) + .setDuration(duration) + .withStartAction { binding.attachmentPanel.isVisible = true } + .start() + } + + private fun hideAttachmentPanel(duration: Long = 250) { + if (isAttachmentPanelVisible.value == true) isAttachmentPanelVisible.value = false + + binding.attachmentPanel.animate() + .alpha(0f) + .translationY(50f) + .setDuration(duration) + .withEndAction { binding.attachmentPanel.isVisible = false } + .start() + } private fun performAction() { if (action.value == Action.RECORD) { - + return } else if (action.value == Action.SEND) { val messageText = binding.message.text.toString().trim() if (messageText.isBlank()) return @@ -214,18 +320,25 @@ class MessagesHistoryFragment : peerId = conversation.id, fromId = UserConfig.userId, date = (date / 1000).toInt(), - randomId = 0 + randomId = 0, + replyMessage = replyMessage.value ) adapter.add(message) - adapter.notifyDataSetChanged() + adapter.notifyItemInserted(adapter.actualSize - 1) binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) binding.message.clear() + val replyMessage = replyMessage.value + + this.replyMessage.value = null + hideAttachmentPanel() + viewModel.sendMessage( peerId = conversation.id, message = messageText, - randomId = 0 + randomId = 0, + replyTo = replyMessage?.id ) { message = message.copyMessage(id = it) } } } @@ -283,17 +396,22 @@ class MessagesHistoryFragment : private fun markMessagesAsImportant(event: MessagesMarkAsImportant) { var changed = false + val positions = mutableListOf() + for (i in adapter.values.indices) { val message = adapter.values[i] if (event.messagesIds.contains(message.id)) { if (!changed) changed = true + + positions.add(i) + adapter.values[i] = message.copyMessage( important = event.important ) } } - if (changed) adapter.notifyDataSetChanged() + if (changed) positions.forEach { adapter.notifyItemChanged(it) } } private fun refreshMessages(event: MessagesLoaded) { @@ -314,21 +432,111 @@ class MessagesHistoryFragment : else binding.recyclerView.scrollToPosition(adapter.lastPosition) } - private fun onItemClick(position: Int) { + private fun onItemClick(position: Int, view: View) { val message = adapter.values[position] if (message.action != null) return - val important = if (message.important) "Unmark as important" else "Mark as important" +// val popupMenu = PopupMenu(requireContext(), view) +// +// val reply = popupMenu.menu.add( +// getString(R.string.message_context_action_reply) +// ) +// +// reply.icon = +// ContextCompat.getDrawable( +// requireContext(), +// R.drawable.ic_attachment_wall_reply +// )?.constantState?.newDrawable()?.also { +// it.setTint( +// ContextCompat.getColor( +// requireContext(), +// R.color.textColorSecondaryVariant +// ) +// ) +// } +// +// val important = popupMenu.menu.add( +// getString( +// if (message.important) R.string.message_context_action_unmark_as_important +// else R.string.message_context_action_mark_as_important +// ) +// ) +// +// important.icon = +// ContextCompat.getDrawable( +// requireContext(), +// R.drawable.ic_star_border +// )?.constantState?.newDrawable()?.also { +// it.setTint( +// ContextCompat.getColor( +// requireContext(), +// R.color.textColorSecondaryVariant +// ) +// ) +// } +// +// popupMenu.setForceShowIcon(true) +// popupMenu.setOnMenuItemClickListener { +// when (it) { +// reply -> { +// val title = when { +// message.isGroup() && message.group.value != null -> message.group.value?.name +// message.isUser() && message.user.value != null -> message.user.value?.fullName +// else -> null +// } +// +// if (replyMessage.value != message) replyMessage.value = message +// +// binding.replyMessageTitle.text = title +// binding.replyMessageText.text = message.text ?: "[no_message]" +// +// if (binding.attachmentPanel.isNotVisible) binding.avatar.performClick() +// true +// } +// +// important -> { +// viewModel.markAsImportant( +// messagesIds = listOf(message.id), +// important = !message.important +// ) +// true +// } +// +// else -> false +// } +// } +// popupMenu.show() - val params = arrayOf(important) + val reply = getString(R.string.message_context_action_reply) + + val important = getString( + if (message.important) R.string.message_context_action_unmark_as_important + else R.string.message_context_action_mark_as_important + ) + + val params = arrayOf(reply, important) val dialog = MaterialAlertDialogBuilder(requireContext()) .setItems(params) { _, which -> - if (which == 0) { - viewModel.markAsImportant( + when (params[which]) { + important -> viewModel.markAsImportant( messagesIds = listOf(message.id), important = !message.important ) + reply -> { + val title = when { + message.isGroup() && message.group.value != null -> message.group.value?.name + message.isUser() && message.user.value != null -> message.user.value?.fullName + else -> null + } + + if (replyMessage.value != message) replyMessage.value = message + + binding.replyMessageTitle.text = title + binding.replyMessageText.text = message.text ?: "[no_message]" + + if (binding.attachmentPanel.isNotVisible) binding.avatar.performClick() + } } } diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt index 2f75959a..e552c313 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt @@ -91,6 +91,7 @@ class MessagesHistoryViewModel @Inject constructor( peerId: Int, message: String? = null, randomId: Int = 0, + replyTo: Int? = null, setId: ((messageId: Int) -> Unit)? = null ) = viewModelScope.launch { makeJob( @@ -99,7 +100,8 @@ class MessagesHistoryViewModel @Inject constructor( MessagesSendRequest( peerId = peerId, randomId = randomId, - message = message + message = message, + replyTo = replyTo ) ) }, diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt index 88f3b77d..e0fad5f3 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt @@ -1,6 +1,7 @@ package com.meloda.fast.screens.messages import android.content.Context +import android.graphics.drawable.ColorDrawable import android.util.Log import android.view.View import android.widget.ImageView @@ -10,7 +11,6 @@ import androidx.appcompat.widget.LinearLayoutCompat import androidx.core.content.ContextCompat import androidx.core.view.isInvisible import androidx.core.view.isVisible -import androidx.core.view.setPadding import coil.load import com.meloda.fast.R import com.meloda.fast.api.VkUtils @@ -20,7 +20,6 @@ import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.attachments.VkSticker import com.meloda.fast.common.AppGlobal -import com.meloda.fast.util.AndroidUtils import com.meloda.fast.widget.BoundedLinearLayout import java.text.SimpleDateFormat import java.util.* @@ -30,13 +29,14 @@ import kotlin.math.roundToInt class MessagesPreparator constructor( private val context: Context, + private val root: View? = null, + private val conversation: VkConversation, private val message: VkMessage, private val prevMessage: VkMessage? = null, private val nextMessage: VkMessage? = null, private val bubble: BoundedLinearLayout? = null, - private val bubbleStroke: View? = null, private val text: TextView? = null, private val avatar: ImageView? = null, private val title: TextView? = null, @@ -65,99 +65,43 @@ class MessagesPreparator constructor( ContextCompat.getDrawable(context, R.drawable.ic_message_out_background) private val backgroundMiddleOut = ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle) - private val backgroundStrokeOut = - ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke) - private val backgroundMiddleStrokeOut = - ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke) +// private val backgroundStrokeOut = +// ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke) +// private val backgroundMiddleStrokeOut = +// ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke) + + private val rootHighlightedColor = + ContextCompat.getColor(context, R.color.n2_100) fun prepare() { - val messageUser: VkUser? = if (message.isUser()) { + val messageUser: VkUser? = (if (message.isUser()) { profiles[message.fromId] - } else null + } else null).also { message.user.value = it } - val messageGroup: VkGroup? = if (message.isGroup()) { + val messageGroup: VkGroup? = (if (message.isGroup()) { groups[message.fromId] - } else null + } else null).also { message.group.value = it } - if (unread != null) { - unread.isVisible = message.isRead(conversation) - } + prepareRootBackground() - if (bubble != null && time != null) { - bubble.setOnClickListener { time.isVisible = !time.isVisible } - } + prepareTime() - if (attachmentContainer != null) { - if (message.attachments.isNullOrEmpty()) { - attachmentContainer.isVisible = false - attachmentContainer.removeAllViews() - } else { - attachmentContainer.isVisible = true - AttachmentInflater( - context = context, - container = attachmentContainer, - message = message, - groups = groups, - profiles = profiles - ).inflate() - } - } + prepareUnreadIndicator() - if (bubble != null) { - val padding = - AndroidUtils.px(if (!message.attachments.isNullOrEmpty()) 4 else 15).roundToInt() + prepareSpacer() - bubble.setPadding(padding) + prepareAttachments() + prepareAttachmentsSpacer() - // TODO: 9/23/2021 use external function - bubble.background = - if (!message.attachments.isNullOrEmpty() && message.attachments!![0] is VkSticker) null - else { - if (message.isOut) { - if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormalOut - else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleOut - else backgroundNormalOut - } else { - if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormalIn - else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleIn - else backgroundNormalIn - } - } - } + prepareBubbleBackground() - // TODO: 9/23/2021 use external function - bubbleStroke?.background = - if (bubble?.background == null) null else { - if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundStrokeOut - else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleStrokeOut - else backgroundStrokeOut - } + prepareText() - if (bubble != null && text != null) { - if (message.text == null) { - text.isVisible = false - bubble.isVisible = !message.attachments.isNullOrEmpty() - bubbleStroke?.isVisible = bubble.isVisible - } else { - text.isVisible = true - bubble.isVisible = true - bubbleStroke?.isVisible = true - text.text = VkUtils.prepareMessageText(message.text) - } - } - - if (avatar != null) { - val avatarUrl = when { - message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200 - message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200 - else -> null - } - - avatar.load(avatarUrl) { crossfade(100) } - } - - spacer?.isVisible = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message) + prepareAvatar( + messageUser = messageUser, + messageGroup = messageGroup + ) if (message.isPeerChat()) { @@ -188,19 +132,98 @@ class MessagesPreparator constructor( title.text = titleString title.measure(0, 0) - - if (bubble != null) { - if (title.isVisible) { - bubble.minimumWidth = title.measuredWidth + 60 - } else { - bubble.minimumWidth = 0 - } - } } + } - attachmentSpacer?.isVisible = - !message.attachments.isNullOrEmpty() && text?.isVisible == true + private fun prepareRootBackground() { + if (root != null) { + root.background = + if (message.isSelected) ColorDrawable(rootHighlightedColor) + else null + } + } + private fun prepareTime() { time?.text = SimpleDateFormat("HH:mm", Locale.getDefault()).format(message.date * 1000L) } + + private fun prepareUnreadIndicator() { + if (unread != null) { + unread.isVisible = message.isRead(conversation) + } + } + + private fun prepareSpacer() { + spacer?.isVisible = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message) + } + + private fun prepareAttachments() { + if (attachmentContainer != null) { + if (message.attachments.isNullOrEmpty()) { + attachmentContainer.isVisible = false + attachmentContainer.removeAllViews() + } else { + attachmentContainer.isVisible = true + AttachmentInflater( + context = context, + container = attachmentContainer, + message = message, + groups = groups, + profiles = profiles + ).inflate() + } + } + } + + private fun prepareAttachmentsSpacer() { + attachmentSpacer?.isVisible = + !message.attachments.isNullOrEmpty() && text?.isVisible == true + } + + private fun prepareBubbleBackground() { + if (bubble != null) { + // TODO: 9/23/2021 use external function + bubble.background = + if (!message.attachments.isNullOrEmpty() && message.attachments!![0] is VkSticker) null + else { + if (message.isOut) { + if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormalOut + else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleOut + else backgroundNormalOut + } else { + if (prevMessage == null || prevMessage.fromId != message.fromId) backgroundNormalIn + else if (prevMessage.fromId == message.fromId && message.date - prevMessage.date < 60) backgroundMiddleIn + else backgroundNormalIn + } + } + } + } + + private fun prepareText() { + if (bubble != null && text != null) { + if (message.text == null) { + text.isVisible = false + bubble.isVisible = !message.attachments.isNullOrEmpty() + } else { + text.isVisible = true + bubble.isVisible = true + text.text = VkUtils.prepareMessageText(message.text) + } + } + } + + private fun prepareAvatar( + messageUser: VkUser? = null, + messageGroup: VkGroup? = null + ) { + if (avatar != null) { + val avatarUrl = when { + message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200 + message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200 + else -> null + } + + avatar.load(avatarUrl) { crossfade(100) } + } + } } diff --git a/app/src/main/res/drawable-v21/ic_star_border.xml b/app/src/main/res/drawable-v21/ic_star_border.xml index 3fc251d1..f341eb01 100644 --- a/app/src/main/res/drawable-v21/ic_star_border.xml +++ b/app/src/main/res/drawable-v21/ic_star_border.xml @@ -1,5 +1,10 @@ - - + + diff --git a/app/src/main/res/drawable/ic_chat_attachment_panel_background.xml b/app/src/main/res/drawable/ic_chat_attachment_panel_background.xml new file mode 100644 index 00000000..4e6ca60c --- /dev/null +++ b/app/src/main/res/drawable/ic_chat_attachment_panel_background.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_image_button_circle_background.xml b/app/src/main/res/drawable/ic_image_button_circle_background.xml new file mode 100644 index 00000000..b50a43d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_image_button_circle_background.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_in_background.xml b/app/src/main/res/drawable/ic_message_in_background.xml index b092122a..e2aa9732 100644 --- a/app/src/main/res/drawable/ic_message_in_background.xml +++ b/app/src/main/res/drawable/ic_message_in_background.xml @@ -6,8 +6,8 @@ + android:topRightRadius="30dp" /> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_message_out_background.xml b/app/src/main/res/drawable/ic_message_out_background.xml index 71d2120f..d2e74ac8 100644 --- a/app/src/main/res/drawable/ic_message_out_background.xml +++ b/app/src/main/res/drawable/ic_message_out_background.xml @@ -2,6 +2,10 @@ + + + + + + diff --git a/app/src/main/res/layout/fragment_conversations.xml b/app/src/main/res/layout/fragment_conversations.xml index cf7d291f..c6af1543 100644 --- a/app/src/main/res/layout/fragment_conversations.xml +++ b/app/src/main/res/layout/fragment_conversations.xml @@ -20,18 +20,9 @@ android:elevation="0dp" app:collapsedTitleTextAppearance="@style/CollapsingToolbarCollapsedTitle" app:expandedTitleTextAppearance="@style/CollapsingToolbarTitle" - app:layout_scrollFlags="scroll|enterAlways|snap" + app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" app:title="Messages"> - - + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_login.xml b/app/src/main/res/layout/fragment_login.xml index 9378d007..b4f85e10 100644 --- a/app/src/main/res/layout/fragment_login.xml +++ b/app/src/main/res/layout/fragment_login.xml @@ -9,6 +9,14 @@ android:layout_height="match_parent" android:orientation="vertical"> + + - + tools:src="@tools:sample/avatars" /> + android:layout_height="match_parent"> + app:tint="?colorSecondary2" /> + + + + + + + + @@ -182,6 +208,69 @@ + + + + + + + + + + + + + + + + + + + android:hint="@string/message_input_hint" + android:singleLine="true" /> - - - - - + android:orientation="vertical"> + + + + + + + + @@ -88,19 +96,6 @@ android:src="@color/a3_200" /> - - - \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_out.xml b/app/src/main/res/layout/item_message_out.xml index 8ac3215d..0b46e0e2 100644 --- a/app/src/main/res/layout/item_message_out.xml +++ b/app/src/main/res/layout/item_message_out.xml @@ -1,5 +1,6 @@ - + android:layout_gravity="center" + android:background="@drawable/ic_message_out_background" + android:clipChildren="true" + android:clipToPadding="true" + android:orientation="vertical"> - + android:layout_height="wrap_content"> @@ -59,30 +58,21 @@ android:id="@+id/attachmentSpacer" android:layout_width="wrap_content" android:layout_height="5dp" + android:layout_below="@+id/text" android:visibility="gone" /> - - - - - + android:visibility="gone" + app:layout_anchor="@+id/text" + app:layout_anchorGravity="bottom" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 378c5604..04e57b21 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,4 +109,12 @@ User post Post Story + Log out + Confirmation + Are you really want to log out? + Yes + No + Reply + Mark as important + Unmark as important diff --git a/gradle.properties b/gradle.properties index 98bed167..e5edfa7b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,21 +1,7 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app"s APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn +org.gradle.jvmargs=-Xmx4096M -XX:MaxPermSize=4096m -Dfile.encoding=UTF-8 +org.gradle.daemon=true +org.gradle.parallel=true +org.gradle.configureondemand=false android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX android.enableJetifier=true -# Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official \ No newline at end of file