From 7c72199d324087d2b46db850c1e81ec613f2b1b1 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Mon, 27 Sep 2021 22:12:37 +0300 Subject: [PATCH 01/15] code saving --- .../com/meloda/fast/api/network/ResultCallFactory.kt | 7 ++++++- .../com/meloda/fast/base/viewmodel/BaseViewModel.kt | 11 +++++------ .../com/meloda/fast/screens/login/LoginFragment.kt | 2 +- .../com/meloda/fast/screens/login/LoginViewModel.kt | 3 +-- .../fast/screens/messages/AttachmentInflater.kt | 4 ++-- 5 files changed, 15 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt b/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt index 1950b7b9..70c5a758 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/ResultCallFactory.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.network import com.meloda.fast.api.VKException +import com.meloda.fast.api.base.ApiError import com.meloda.fast.api.base.ApiResponse import okhttp3.Request import okio.IOException @@ -93,7 +94,6 @@ internal class ResultCall(proxy: Call) : CallDelegate>(proxy) if (result is Answer.Error && isVkException) if (checkErrors(call, result)) return - callback.onResponse(proxy, Response.success(result)) } @@ -105,6 +105,11 @@ internal class ResultCall(proxy: Call) : CallDelegate>(proxy) } private fun checkErrors(call: Call, result: Answer.Error): Boolean { + if (result.throwable is ApiError) { + onFailure(call, result.throwable) + return true + } + val json = JSONObject(result.throwable.message ?: "{}") return if (json.has("error")) { diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt index 4892ca01..97c3220b 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt @@ -30,13 +30,12 @@ abstract class BaseViewModel : ViewModel() { is Answer.Success -> onAnswer(response.data) is Answer.Error -> { checkErrors(response.throwable) - onError?.invoke(response.throwable) - ?: sendEvent( - ErrorEvent( - response.throwable.message - ?: unknownErrorDefaultText - ) + onError?.invoke(response.throwable) ?: sendEvent( + ErrorEvent( + response.throwable.message + ?: unknownErrorDefaultText ) + ) } } }.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } } diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt index 7893cec7..1f27553b 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 @@ -63,7 +63,7 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo super.onEvent(event) when (event) { - is ShowError -> showErrorSnackbar(event.errorDescription) + is ErrorEvent -> showErrorSnackbar(event.errorText) is CaptchaEvent -> showCaptchaDialog(event.sid, event.image) is ValidationEvent -> showValidationRequired(event.sid) is SuccessAuth -> goToMain(event.haveAuthorized) diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt index c4a613e1..93530186 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt @@ -53,6 +53,7 @@ class LoginViewModel @Inject constructor( onError = { if (it !is VKException) return@makeJob + // TODO: 9/27/2021 use `delay` parameter twoFaCode?.let { sendEvent(CodeSent) } }, onStart = { sendEvent(StartProgressEvent) }, @@ -70,8 +71,6 @@ class LoginViewModel @Inject constructor( } -data class ShowError(val errorDescription: String) : VKEvent() - object CodeSent : VKEvent() data class SuccessAuth(val haveAuthorized: Boolean = true) : VKEvent() \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt index 581ec9a3..36e81833 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 @@ -223,7 +223,7 @@ class AttachmentInflater constructor( binding.caption.isVisible = !link.caption.isNullOrBlank() binding.preview.shapeAppearanceModel.toBuilder() - .setAllCornerSizes(40f) + .setAllCornerSizes(AndroidUtils.px(20)) .build() .let { binding.preview.shapeAppearanceModel = it @@ -282,7 +282,7 @@ class AttachmentInflater constructor( binding.avatar.isVisible = group != null || user != null binding.avatar.shapeAppearanceModel.toBuilder() - .setAllCornerSizes(40f) + .setAllCornerSizes(AndroidUtils.px(20)) .build() .let { binding.avatar.shapeAppearanceModel = it From 9e074dd5ad501a9c2a110bdba34867115de434e2 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sat, 2 Oct 2021 20:13:27 +0300 Subject: [PATCH 02/15] =?UTF-8?q?=D1=8F=20=D0=BD=D0=B5=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BC=D0=BD=D1=8E,=20=D1=87=D1=82=D0=BE=20=D1=82=D1=83=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle.kts | 9 +- .../kotlin/com/meloda/fast/api/UserConfig.kt | 9 + .../kotlin/com/meloda/fast/api/VKConstants.kt | 2 + .../kotlin/com/meloda/fast/api/VkUtils.kt | 6 + .../com/meloda/fast/api/model/VkMessage.kt | 21 +- .../fast/api/model/attachments/VkPhoto.kt | 51 +++- .../fast/api/model/base/BaseVkMessage.kt | 4 +- .../fast/api/network/AuthInterceptor.kt | 1 + .../meloda/fast/base/adapter/BaseAdapter.kt | 2 +- .../com/meloda/fast/base/adapter/Items.kt | 15 + .../com/meloda/fast/database/AppDatabase.kt | 2 +- .../com/meloda/fast/extensions/Extensions.kt | 6 +- .../conversations/ConversationsFragment.kt | 105 +++++-- .../conversations/ConversationsViewModel.kt | 16 +- .../fast/screens/login/LoginFragment.kt | 93 +++++++ .../screens/messages/AttachmentInflater.kt | 23 +- .../messages/MessagesHistoryAdapter.kt | 45 +-- .../messages/MessagesHistoryFragment.kt | 262 ++++++++++++++++-- .../messages/MessagesHistoryViewModel.kt | 4 +- .../screens/messages/MessagesPreparator.kt | 207 ++++++++------ .../main/res/drawable-v21/ic_star_border.xml | 13 +- .../ic_chat_attachment_panel_background.xml | 11 + .../ic_image_button_circle_background.xml | 9 + .../res/drawable/ic_message_in_background.xml | 4 +- .../drawable/ic_message_out_background.xml | 4 + .../ic_message_out_background_middle.xml | 4 + .../main/res/drawable/ic_round_close_20.xml | 9 + .../res/layout/fragment_conversations.xml | 51 +++- app/src/main/res/layout/fragment_login.xml | 8 + .../res/layout/fragment_messages_history.xml | 104 ++++++- .../layout/item_message_attachment_link.xml | 3 +- app/src/main/res/layout/item_message_in.xml | 59 ++-- app/src/main/res/layout/item_message_out.xml | 46 ++- app/src/main/res/values/strings.xml | 8 + gradle.properties | 22 +- 35 files changed, 945 insertions(+), 293 deletions(-) create mode 100644 app/src/main/kotlin/com/meloda/fast/base/adapter/Items.kt create mode 100644 app/src/main/res/drawable/ic_chat_attachment_panel_background.xml create mode 100644 app/src/main/res/drawable/ic_image_button_circle_background.xml create mode 100644 app/src/main/res/drawable/ic_round_close_20.xml 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 From 7c1a7d8a89d92ba06f21fe896f3031de612d905b Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Fri, 8 Oct 2021 12:55:22 +0300 Subject: [PATCH 03/15] messages pin & unpin feature fix avatars and titles visual improvements other bugfixes & minor changes --- .../meloda/fast/api/model/VkConversation.kt | 33 ++- .../com/meloda/fast/api/model/VkMessage.kt | 6 + .../fast/api/model/base/BaseVkConversation.kt | 3 +- .../fast/api/model/request/MessagesRequest.kt | 22 ++ .../fast/api/network/{VKUrls.kt => VkUrls.kt} | 4 +- .../network/datasource/MessagesDataSource.kt | 15 +- .../meloda/fast/api/network/repo/AuthRepo.kt | 6 +- .../api/network/repo/ConversationsRepo.kt | 4 +- .../fast/api/network/repo/MessagesRepo.kt | 19 +- .../meloda/fast/api/network/repo/UsersRepo.kt | 4 +- .../meloda/fast/base/BaseViewModelFragment.kt | 4 +- .../fast/base/viewmodel/BaseViewModel.kt | 33 ++- .../com/meloda/fast/base/viewmodel/Events.kt | 16 +- .../com/meloda/fast/database/AppDatabase.kt | 2 +- .../conversations/ConversationsFragment.kt | 4 +- .../conversations/ConversationsViewModel.kt | 15 +- .../fast/screens/login/LoginFragment.kt | 2 +- .../fast/screens/login/LoginViewModel.kt | 19 +- .../screens/messages/AttachmentInflater.kt | 13 + .../messages/MessagesHistoryAdapter.kt | 10 +- .../messages/MessagesHistoryFragment.kt | 277 +++++++++--------- .../messages/MessagesHistoryViewModel.kt | 83 ++++-- .../screens/messages/MessagesPreparator.kt | 32 +- .../fast/screens/photos/PhotoViewFragment.kt | 48 +++ .../fast/screens/photos/PhotoViewViewModel.kt | 22 ++ .../kotlin/com/meloda/fast/util/TimeUtils.kt | 2 + .../main/res/drawable/ic_round_done_24.xml | 9 + .../res/layout/fragment_messages_history.xml | 9 +- app/src/main/res/layout/item_message_in.xml | 27 +- app/src/main/res/layout/item_message_out.xml | 57 ++-- app/src/main/res/navigation/messages.xml | 13 +- app/src/main/res/values/strings.xml | 4 + 32 files changed, 504 insertions(+), 313 deletions(-) rename app/src/main/kotlin/com/meloda/fast/api/network/{VKUrls.kt => VkUrls.kt} (87%) create mode 100644 app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt create mode 100644 app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt create mode 100644 app/src/main/res/drawable/ic_round_done_24.xml diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt index 42d221e6..76ca6d6f 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt @@ -10,27 +10,28 @@ import kotlinx.parcelize.Parcelize @Parcelize data class VkConversation( @PrimaryKey(autoGenerate = false) - val id: Int, - val ownerId: Int?, - val title: String?, - val photo200: String?, - val type: String, - val callInProgress: Boolean, - val isPhantom: Boolean, - val lastConversationMessageId: Int, - val inRead: Int, - val outRead: Int, - val isMarkedUnread: Boolean, - val lastMessageId: Int, - val unreadCount: Int?, - val membersCount: Int?, - val isPinned: Boolean, + var id: Int, + var ownerId: Int?, + var title: String?, + var photo200: String?, + var type: String, + var callInProgress: Boolean, + var isPhantom: Boolean, + var lastConversationMessageId: Int, + var inRead: Int, + var outRead: Int, + var isMarkedUnread: Boolean, + var lastMessageId: Int, + var unreadCount: Int?, + var membersCount: Int?, + var isPinned: Boolean, + var canChangePin: Boolean, @Embedded(prefix = "pinnedMessage_") var pinnedMessage: VkMessage? = null, @Embedded(prefix = "lastMessage_") - var lastMessage: VkMessage? = null + var lastMessage: VkMessage? = null, ) : Parcelable { fun isChat() = type == "chat" 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 cd0beaca..424c43e1 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 @@ -4,8 +4,10 @@ import androidx.lifecycle.MutableLiveData import androidx.room.Entity import androidx.room.Ignore import androidx.room.PrimaryKey +import com.meloda.fast.api.UserConfig import com.meloda.fast.api.model.attachments.VkAttachment import com.meloda.fast.base.adapter.SelectableItem +import com.meloda.fast.util.TimeUtils import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -58,6 +60,10 @@ data class VkMessage( return Action.parse(action) } + fun canEdit() = + fromId == UserConfig.userId && + (System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.ONE_DAY_IN_SECONDS) + fun copyMessage( id: Int = this.id, text: String? = this.text, diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkConversation.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkConversation.kt index 25490fa1..8c7177f2 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkConversation.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/BaseVkConversation.kt @@ -40,7 +40,8 @@ data class BaseVkConversation( unreadCount = unread_count, membersCount = chat_settings?.members_count, ownerId = chat_settings?.owner_id, - isPinned = sort_id.major_id > 0 + isPinned = sort_id.major_id > 0, + canChangePin = chat_settings?.acl?.can_change_pin == true ).apply { this.lastMessage = lastMessage this.pinnedMessage = chat_settings?.pinned_message?.asVkMessage() diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt index fda2f97f..1cb01f21 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt @@ -70,6 +70,28 @@ data class MessagesMarkAsImportantRequest( } +@Parcelize +data class MessagesPinMessageRequest( + val peerId: Int, + val messageId: Int? = null, + val conversationMessageId: Int? = null +) : Parcelable { + + val map + get() = mutableMapOf( + "peer_id" to peerId.toString() + ).apply { + messageId?.let { this["message_id"] = it.toString() } + conversationMessageId?.let { this["conversation_message_id"] = it.toString() } + } + +} + +@Parcelize +data class MessagesUnPinMessageRequest(val peerId: Int) : Parcelable { + val map get() = mutableMapOf("peer_id" to peerId.toString()) +} + @Parcelize data class MessagesGetLongPollServerRequest( val needPts: Boolean, diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt similarity index 87% rename from app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt index d0476e13..ea4f2c5f 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/VKUrls.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt @@ -1,6 +1,6 @@ package com.meloda.fast.api.network -object VKUrls { +object VkUrls { const val OAUTH = "https://oauth.vk.com" const val API = "https://api.vk.com/method" @@ -22,6 +22,8 @@ object VKUrls { const val GetHistory = "$API/messages.getHistory" const val Send = "$API/messages.send" const val MarkAsImportant = "$API/messages.markAsImportant" + const val Pin = "$API/messages.pin" + const val Unpin = "$API/messages.unpin" const val GetLongPollServer = "$API/messages.getLongPollServer" const val GetLongPollHistory = "$API/messages.getLongPollHistory" } diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt index 5995d515..472004da 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt @@ -1,10 +1,7 @@ package com.meloda.fast.api.network.datasource import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.request.MessagesGetHistoryRequest -import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest -import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest -import com.meloda.fast.api.model.request.MessagesSendRequest +import com.meloda.fast.api.model.request.* import com.meloda.fast.api.network.repo.MessagesRepo import com.meloda.fast.database.dao.MessagesDao import javax.inject.Inject @@ -26,8 +23,14 @@ class MessagesDataSource @Inject constructor( suspend fun getLongPollServer(params: MessagesGetLongPollServerRequest) = repo.getLongPollServer(params.map) - suspend fun storeMessages(messages: List) = dao.insert(messages) + suspend fun pin(params: MessagesPinMessageRequest) = + repo.pin(params.map) - suspend fun getCachedMessages(peerId: Int) = dao.getByPeerId(peerId) + suspend fun unpin(params: MessagesUnPinMessageRequest) = + repo.unpin(params.map) + + suspend fun store(messages: List) = dao.insert(messages) + + suspend fun getCached(peerId: Int) = dao.getByPeerId(peerId) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt index 60fed0f8..e0b72cba 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt @@ -1,6 +1,6 @@ package com.meloda.fast.api.network.repo -import com.meloda.fast.api.network.VKUrls +import com.meloda.fast.api.network.VkUrls import com.meloda.fast.api.model.response.ResponseAuthDirect import com.meloda.fast.api.network.Answer import com.meloda.fast.api.model.response.ResponseSendSms @@ -8,10 +8,10 @@ import retrofit2.http.* interface AuthRepo { - @GET(VKUrls.Auth.DirectAuth) + @GET(VkUrls.Auth.DirectAuth) suspend fun auth(@QueryMap param: Map): Answer - @GET(VKUrls.Auth.SendSms) + @GET(VkUrls.Auth.SendSms) suspend fun sendSms(@Query("sid") validationSid: String): Answer } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt index 9f0c8d4c..b394e85a 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt @@ -2,7 +2,7 @@ package com.meloda.fast.api.network.repo import com.meloda.fast.api.base.ApiResponse import com.meloda.fast.api.network.Answer -import com.meloda.fast.api.network.VKUrls +import com.meloda.fast.api.network.VkUrls import com.meloda.fast.api.model.response.ConversationsGetResponse import retrofit2.http.FieldMap import retrofit2.http.FormUrlEncoded @@ -11,7 +11,7 @@ import retrofit2.http.POST interface ConversationsRepo { @FormUrlEncoded - @POST(VKUrls.Conversations.Get) + @POST(VkUrls.Conversations.Get) suspend fun getAllChats(@FieldMap params: Map): Answer> } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt index 831d4c49..1b4c6717 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt @@ -2,9 +2,10 @@ package com.meloda.fast.api.network.repo import com.meloda.fast.api.base.ApiResponse import com.meloda.fast.api.model.base.BaseVkLongPoll +import com.meloda.fast.api.model.base.BaseVkMessage import com.meloda.fast.api.model.response.MessagesGetHistoryResponse import com.meloda.fast.api.network.Answer -import com.meloda.fast.api.network.VKUrls +import com.meloda.fast.api.network.VkUrls import retrofit2.http.FieldMap import retrofit2.http.FormUrlEncoded import retrofit2.http.POST @@ -12,19 +13,27 @@ import retrofit2.http.POST interface MessagesRepo { @FormUrlEncoded - @POST(VKUrls.Messages.GetHistory) + @POST(VkUrls.Messages.GetHistory) suspend fun getHistory(@FieldMap params: Map): Answer> @FormUrlEncoded - @POST(VKUrls.Messages.Send) + @POST(VkUrls.Messages.Send) suspend fun send(@FieldMap params: Map): Answer> @FormUrlEncoded - @POST(VKUrls.Messages.MarkAsImportant) + @POST(VkUrls.Messages.MarkAsImportant) suspend fun markAsImportant(@FieldMap params: Map): Answer>> @FormUrlEncoded - @POST(VKUrls.Messages.GetLongPollServer) + @POST(VkUrls.Messages.GetLongPollServer) suspend fun getLongPollServer(@FieldMap params: Map): Answer> + @FormUrlEncoded + @POST(VkUrls.Messages.Pin) + suspend fun pin(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Messages.Unpin) + suspend fun unpin(@FieldMap params: Map): Answer> + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/UsersRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/repo/UsersRepo.kt index 782cb32f..f11a408d 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/UsersRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/repo/UsersRepo.kt @@ -3,7 +3,7 @@ package com.meloda.fast.api.network.repo import com.meloda.fast.api.base.ApiResponse import com.meloda.fast.api.model.base.BaseVkUser import com.meloda.fast.api.network.Answer -import com.meloda.fast.api.network.VKUrls +import com.meloda.fast.api.network.VkUrls import retrofit2.http.FieldMap import retrofit2.http.FormUrlEncoded import retrofit2.http.POST @@ -11,7 +11,7 @@ import retrofit2.http.POST interface UsersRepo { @FormUrlEncoded - @POST(VKUrls.Users.GetById) + @POST(VkUrls.Users.GetById) suspend fun getById( @FieldMap params: Map? ): Answer>> diff --git a/app/src/main/kotlin/com/meloda/fast/base/BaseViewModelFragment.kt b/app/src/main/kotlin/com/meloda/fast/base/BaseViewModelFragment.kt index cb21a3e1..40ddc1f0 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/BaseViewModelFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/BaseViewModelFragment.kt @@ -11,7 +11,7 @@ import com.meloda.fast.activity.MainActivity import com.meloda.fast.api.UserConfig import com.meloda.fast.base.viewmodel.BaseViewModel import com.meloda.fast.base.viewmodel.IllegalTokenEvent -import com.meloda.fast.base.viewmodel.VKEvent +import com.meloda.fast.base.viewmodel.VkEvent import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach @@ -30,7 +30,7 @@ abstract class BaseViewModelFragment : BaseFragment { } } - protected open fun onEvent(event: VKEvent) { + protected open fun onEvent(event: VkEvent) { if (event is IllegalTokenEvent) { Toast.makeText( requireContext(), R.string.authorization_failed, Toast.LENGTH_LONG diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt index 97c3220b..8ce65c7e 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/BaseViewModel.kt @@ -15,7 +15,7 @@ abstract class BaseViewModel : ViewModel() { var unknownErrorDefaultText: String = "" - protected val tasksEventChannel = Channel() + protected val tasksEventChannel = Channel() val tasksEvent = tasksEventChannel.receiveAsFlow() protected fun makeJob( @@ -25,22 +25,35 @@ abstract class BaseViewModel : ViewModel() { onEnd: (suspend () -> Unit)? = null, onError: (suspend (Throwable) -> Unit)? = null ) = viewModelScope.launch { - onStart?.invoke() + onStart?.invoke() ?: onStart() when (val response = job()) { is Answer.Success -> onAnswer(response.data) is Answer.Error -> { checkErrors(response.throwable) - onError?.invoke(response.throwable) ?: sendEvent( - ErrorEvent( - response.throwable.message - ?: unknownErrorDefaultText - ) - ) + onError?.invoke(response.throwable) ?: onError(response.throwable) } } - }.also { it.invokeOnCompletion { viewModelScope.launch { onEnd?.invoke() } } } + }.also { + it.invokeOnCompletion { + viewModelScope.launch { + onEnd?.invoke() ?: onStop() + } + } + } - protected suspend fun sendEvent(event: T) = tasksEventChannel.send(event) + protected suspend fun onStart() { + sendEvent(StartProgressEvent) + } + + protected suspend fun onStop() { + sendEvent(StopProgressEvent) + } + + protected suspend fun onError(throwable: Throwable) { + sendEvent(ErrorEvent(throwable.message ?: unknownErrorDefaultText)) + } + + protected suspend fun sendEvent(event: T) = tasksEventChannel.send(event) private suspend fun checkErrors(throwable: Throwable) { when (throwable) { diff --git a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt index 0bac43e5..52f55dfa 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/viewmodel/Events.kt @@ -5,15 +5,15 @@ data class ShowDialogInfoEvent( val message: String, val positiveBtn: String? = null, val negativeBtn: String? = null -) : VKEvent() +) : VkEvent() -data class ErrorEvent(val errorText: String) : VKEvent() +data class ErrorEvent(val errorText: String) : VkEvent() -object IllegalTokenEvent : VKEvent() -data class CaptchaEvent(val sid: String, val image: String) : VKEvent() -data class ValidationEvent(val sid: String) : VKEvent() +object IllegalTokenEvent : VkEvent() +data class CaptchaEvent(val sid: String, val image: String) : VkEvent() +data class ValidationEvent(val sid: String) : VkEvent() -object StartProgressEvent : VKEvent() -object StopProgressEvent : VKEvent() +object StartProgressEvent : VkEvent() +object StopProgressEvent : VkEvent() -abstract class VKEvent \ No newline at end of file +abstract class VkEvent \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt b/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt index fb47317e..e8d5c859 100644 --- a/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt +++ b/app/src/main/kotlin/com/meloda/fast/database/AppDatabase.kt @@ -19,7 +19,7 @@ import com.meloda.fast.database.dao.UsersDao VkUser::class, VkGroup::class ], - version = 25, + version = 26, exportSchema = false, ) @TypeConverters(Converters::class) 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 63bb37a3..0071d184 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 @@ -24,7 +24,7 @@ import com.meloda.fast.api.model.VkConversation import com.meloda.fast.base.BaseViewModelFragment import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent -import com.meloda.fast.base.viewmodel.VKEvent +import com.meloda.fast.base.viewmodel.VkEvent import com.meloda.fast.common.AppGlobal import com.meloda.fast.common.AppSettings import com.meloda.fast.common.dataStore @@ -185,7 +185,7 @@ class ConversationsFragment : .show() } - override fun onEvent(event: VKEvent) { + override fun onEvent(event: VkEvent) { super.onEvent(event) when (event) { is ConversationsLoaded -> refreshConversations(event) 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 9e83ebd7..83e8906b 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 @@ -11,9 +11,7 @@ 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 -import com.meloda.fast.base.viewmodel.VKEvent +import com.meloda.fast.base.viewmodel.VkEvent import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -66,13 +64,8 @@ class ConversationsViewModel @Inject constructor( ) ) } - }, - onError = { - val er = it - throw it - }, - onStart = { sendEvent(StartProgressEvent) }, - onEnd = { sendEvent(StopProgressEvent) }) + } + ) } fun loadProfileUser() = viewModelScope.launch { @@ -95,4 +88,4 @@ data class ConversationsLoaded( val conversations: List, val profiles: HashMap, val groups: HashMap -) : VKEvent() +) : VkEvent() 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 f96a9bcc..ce4b2417 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 @@ -70,7 +70,7 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo binding.loginInput.clearFocus() } - override fun onEvent(event: VKEvent) { + override fun onEvent(event: VkEvent) { super.onEvent(event) when (event) { diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt index 93530186..330344e9 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt @@ -51,26 +51,25 @@ class LoginViewModel @Inject constructor( sendEvent(SuccessAuth()) }, onError = { - if (it !is VKException) return@makeJob + if (it !is VKException) { + onError(it) + return@makeJob + } // TODO: 9/27/2021 use `delay` parameter twoFaCode?.let { sendEvent(CodeSent) } - }, - onStart = { sendEvent(StartProgressEvent) }, - onEnd = { sendEvent(StopProgressEvent) } + } ) } fun sendSms(validationSid: String) = viewModelScope.launch { makeJob({ dataSource.sendSms(validationSid) }, - onAnswer = { sendEvent(CodeSent) }, - onError = {}, - onStart = {}, - onEnd = {}) + onAnswer = { sendEvent(CodeSent) } + ) } } -object CodeSent : VKEvent() +object CodeSent : VkEvent() -data class SuccessAuth(val haveAuthorized: Boolean = true) : VKEvent() \ No newline at end of file +data class SuccessAuth(val haveAuthorized: Boolean = true) : VkEvent() \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt index 6f3343eb..5e231f39 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 @@ -45,6 +45,13 @@ class AttachmentInflater constructor( private val playColor = ContextCompat.getColor(context, R.color.a3_700) private val playBackgroundColor = ContextCompat.getColor(context, R.color.a3_200) + var photoClickListener: ((url: String) -> Unit)? = null + + fun setPhotoClickListener(unit: ((url: String) -> Unit)?): AttachmentInflater { + this.photoClickListener = unit + return this + } + fun inflate() { if (message.attachments.isNullOrEmpty()) return attachments = message.attachments!! @@ -114,6 +121,12 @@ class AttachmentInflater constructor( scaleType = ImageView.ScaleType.CENTER_CROP } + if (photoClickListener != null) { + newPhoto.setOnClickListener { photoClickListener?.invoke(size.url) } + } else { + newPhoto.setOnClickListener(null) + } + val spacer = Space(context).also { it.layoutParams = LinearLayoutCompat.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, 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 d8b22578..594ef13a 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 @@ -6,6 +6,7 @@ import android.graphics.drawable.ColorDrawable import android.view.View import android.view.ViewGroup import android.widget.AdapterView +import android.widget.Toast import androidx.appcompat.widget.LinearLayoutCompat import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil @@ -16,6 +17,7 @@ import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkUser +import com.meloda.fast.api.model.attachments.VkAttachment import com.meloda.fast.api.model.attachments.VkPhoto import com.meloda.fast.base.adapter.BaseAdapter import com.meloda.fast.base.adapter.BaseHolder @@ -36,6 +38,8 @@ class MessagesHistoryAdapter constructor( var onItemClickListener: ((position: Int, view: View) -> Unit)? = null + var attachmentClickListener: ((attachment: VkAttachment) -> Unit)? = null + override fun getItemViewType(position: Int): Int { when { isPositionHeader(position) -> return HEADER @@ -123,6 +127,8 @@ class MessagesHistoryAdapter constructor( prevMessage = prevMessage, nextMessage = nextMessage, + title = binding.title, + avatar = binding.avatar, bubble = binding.bubble, text = binding.text, @@ -133,7 +139,9 @@ class MessagesHistoryAdapter constructor( profiles = profiles, groups = groups - ).prepare() + ).setPhotoClickListener { + Toast.makeText(context, "Photo url: $it", Toast.LENGTH_LONG).show() + }.prepare() } } 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 631589f4..fafca889 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 @@ -27,10 +27,9 @@ import com.meloda.fast.api.model.VkUser import com.meloda.fast.base.BaseViewModelFragment import com.meloda.fast.base.viewmodel.StartProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent -import com.meloda.fast.base.viewmodel.VKEvent +import com.meloda.fast.base.viewmodel.VkEvent import com.meloda.fast.databinding.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 @@ -49,7 +48,7 @@ class MessagesHistoryFragment : private val action = MutableLiveData() private enum class Action { - RECORD, SEND + RECORD, SEND, EDIT } private val user: VkUser? by lazy { @@ -71,14 +70,15 @@ class MessagesHistoryFragment : } } - private val replyMessage = MutableLiveData() - private val isAttachmentPanelVisible = MutableLiveData(false) - private var timestampTimer: Timer? = null + private lateinit var attachmentController: AttachmentPanelController + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + attachmentController = AttachmentPanelController().init() + val title = when { conversation.isChat() -> conversation.title conversation.isUser() -> user?.toString() @@ -167,8 +167,11 @@ class MessagesHistoryFragment : it.toString().isNotBlank() val newValue = - if (canSend) Action.SEND - else Action.RECORD + when { + attachmentController.isEditing -> Action.EDIT + canSend -> Action.SEND + else -> Action.RECORD + } if (action.value != newValue) action.value = newValue } @@ -193,30 +196,21 @@ class MessagesHistoryFragment : Action.SEND -> { binding.action.setImageResource(R.drawable.ic_round_send_24) } + Action.EDIT -> { + binding.action.setImageResource(R.drawable.ic_round_done_24) + } else -> return@observe } } - isAttachmentPanelVisible.observe(viewLifecycleOwner) { + attachmentController.isPanelVisible.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 message = attachmentController.message.value ?: return@c val index = adapter.values.indexOf(message) if (index == -1) return@c @@ -225,9 +219,8 @@ class MessagesHistoryFragment : } binding.dismissReply.setOnClickListener { - if (replyMessage.value != null) replyMessage.value = null - - hideAttachmentPanel() + if (attachmentController.message.value != null) + attachmentController.message.value = null } } @@ -282,28 +275,6 @@ class MessagesHistoryFragment : 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 @@ -321,7 +292,7 @@ class MessagesHistoryFragment : fromId = UserConfig.userId, date = (date / 1000).toInt(), randomId = 0, - replyMessage = replyMessage.value + replyMessage = attachmentController.message.value ) adapter.add(message) @@ -329,10 +300,8 @@ class MessagesHistoryFragment : binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) binding.message.clear() - val replyMessage = replyMessage.value - - this.replyMessage.value = null - hideAttachmentPanel() + val replyMessage = attachmentController.message.value + attachmentController.message.value = null viewModel.sendMessage( peerId = conversation.id, @@ -343,12 +312,13 @@ class MessagesHistoryFragment : } } - override fun onEvent(event: VKEvent) { + override fun onEvent(event: VkEvent) { super.onEvent(event) when (event) { is MessagesMarkAsImportant -> markMessagesAsImportant(event) is MessagesLoaded -> refreshMessages(event) + is MessagesPin -> conversation.pinnedMessage = event.message is StartProgressEvent -> onProgressStarted() is StopProgressEvent -> onProgressStopped() } @@ -436,106 +406,67 @@ class MessagesHistoryFragment : val message = adapter.values[position] if (message.action != null) return -// 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 time = getString( + R.string.time_format, + SimpleDateFormat( + "dd.MM.yyyy, HH:mm:ss", + Locale.getDefault() + ).format(message.date * 1000L) + ) val reply = getString(R.string.message_context_action_reply) + val isMessageAlreadyPinned = message.id == conversation.pinnedMessage?.id + + val pin = getString( + if (isMessageAlreadyPinned) R.string.message_context_action_unpin + else R.string.message_context_action_pin + ) + + val edit = getString(R.string.message_context_action_edit) + val 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 params = mutableListOf() + params.add(reply) + + if (conversation.canChangePin) { + params.add(pin) + } + + if (message.canEdit()) { + params.add(edit) + } + + params.add(important) + + val arrayParams = params.toTypedArray() val dialog = MaterialAlertDialogBuilder(requireContext()) - .setItems(params) { _, which -> + .setTitle(time) + .setItems(arrayParams) { _, which -> 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 (attachmentController.message.value != message) + attachmentController.message.value = message + } + pin -> viewModel.pinMessage( + peerId = conversation.id, + messageId = message.id, + pin = !isMessageAlreadyPinned + ) + edit -> { + attachmentController.isEditing = true - 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() + if (attachmentController.message.value != message) + attachmentController.message.value = message } } } @@ -549,4 +480,78 @@ class MessagesHistoryFragment : return true } + private inner class AttachmentPanelController { + val isPanelVisible = MutableLiveData(false) + val message = MutableLiveData() + + var isEditing = false + + fun init(): AttachmentPanelController { + message.observe(viewLifecycleOwner) { value -> + if (value != null) { + applyMessage(value) + } else { + clearMessage() + } + } + + message.value = null + return this + } + + private fun applyMessage(message: VkMessage) { + showPanel() + + val title = when { + message.isGroup() && message.group.value != null -> message.group.value?.name + message.isUser() && message.user.value != null -> message.user.value?.fullName + else -> null + } + + binding.replyMessageTitle.text = title + binding.replyMessageText.text = message.text ?: "[no_message]" + + if (isEditing) { + binding.message.setText(message.text ?: "[no_message]") + } + } + + private fun clearMessage() { + hidePanel() + + binding.replyMessageTitle.clear() + binding.replyMessageText.clear() + + if (isEditing) { + isEditing = false + binding.message.clear() + } + } + + private fun showPanel(duration: Long = 250) { + if (attachmentController.isPanelVisible.value == false) + attachmentController.isPanelVisible.value = true + + binding.attachmentPanel.animate() + .translationY(0f) + .alpha(1f) + .setDuration(duration) + .withStartAction { binding.attachmentPanel.isVisible = true } + .start() + } + + private fun hidePanel(duration: Long = 250) { + if (attachmentController.isPanelVisible.value == true) + attachmentController.isPanelVisible.value = false + + binding.attachmentPanel.animate() + .alpha(0f) + .translationY(50f) + .setDuration(duration) + .withEndAction { binding.attachmentPanel.isVisible = false } + .start() + } + + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt index e552c313..f425a01b 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt @@ -6,28 +6,24 @@ import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.request.MessagesGetHistoryRequest -import com.meloda.fast.api.model.request.MessagesMarkAsImportantRequest -import com.meloda.fast.api.model.request.MessagesSendRequest +import com.meloda.fast.api.model.request.* import com.meloda.fast.api.network.datasource.MessagesDataSource import com.meloda.fast.base.viewmodel.BaseViewModel -import com.meloda.fast.base.viewmodel.StartProgressEvent -import com.meloda.fast.base.viewmodel.StopProgressEvent -import com.meloda.fast.base.viewmodel.VKEvent +import com.meloda.fast.base.viewmodel.VkEvent import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class MessagesHistoryViewModel @Inject constructor( - private val dataSource: MessagesDataSource + private val messages: MessagesDataSource ) : BaseViewModel() { fun loadHistory( peerId: Int ) = viewModelScope.launch { makeJob({ - dataSource.getHistory( + messages.getHistory( MessagesGetHistoryRequest( count = 30, peerId = peerId, @@ -53,18 +49,18 @@ class MessagesHistoryViewModel @Inject constructor( } } - val messages = hashMapOf() + val hashMessages = hashMapOf() response.items.forEach { baseMessage -> - baseMessage.asVkMessage().let { message -> messages[message.id] = message } + baseMessage.asVkMessage().let { message -> hashMessages[message.id] = message } } - dataSource.storeMessages(messages.values.toList()) + messages.store(hashMessages.values.toList()) val conversations = hashMapOf() response.conversations?.let { baseConversations -> baseConversations.forEach { baseConversation -> baseConversation.asVkConversation( - messages[baseConversation.last_message_id] + hashMessages[baseConversation.last_message_id] ).let { conversation -> conversations[conversation.id] = conversation } } } @@ -75,16 +71,10 @@ class MessagesHistoryViewModel @Inject constructor( profiles = profiles, groups = groups, conversations = conversations, - messages = messages.values.toList() + messages = hashMessages.values.toList() ) ) - }, - onError = { - val throwable = it - throw it - }, - onStart = { sendEvent(StartProgressEvent) }, - onEnd = { sendEvent(StopProgressEvent) }) + }) } fun sendMessage( @@ -96,7 +86,7 @@ class MessagesHistoryViewModel @Inject constructor( ) = viewModelScope.launch { makeJob( { - dataSource.send( + messages.send( MessagesSendRequest( peerId = peerId, randomId = randomId, @@ -108,10 +98,6 @@ class MessagesHistoryViewModel @Inject constructor( onAnswer = { val response = it.response ?: return@makeJob setId?.invoke(response) - }, - onError = { - val throwable = it - val i = 0 }) } @@ -120,7 +106,7 @@ class MessagesHistoryViewModel @Inject constructor( important: Boolean ) = viewModelScope.launch { makeJob({ - dataSource.markAsImportant( + messages.markAsImportant( MessagesMarkAsImportantRequest( messagesIds = messagesIds, important = important @@ -135,13 +121,39 @@ class MessagesHistoryViewModel @Inject constructor( important = important ) ) - }, - onError = { - val throwable = it - val i = 0 }) } + fun pinMessage( + peerId: Int, + messageId: Int? = null, + conversationMessageId: Int? = null, + pin: Boolean + ) = viewModelScope.launch { + if (pin) { + makeJob({ + messages.pin( + MessagesPinMessageRequest( + peerId = peerId, + messageId = messageId, + conversationMessageId = conversationMessageId + ) + ) + }, + onAnswer = { + val response = it.response ?: return@makeJob + sendEvent(MessagesPin(response.asVkMessage())) + } + ) + } else { + makeJob({ messages.unpin(MessagesUnPinMessageRequest(peerId = peerId)) }, + onAnswer = { + println("Fast::MessagesHistoryViewModel::unPin::Response::${it.response}") + sendEvent(MessagesUnpin) + } + ) + } + } } data class MessagesLoaded( @@ -150,9 +162,16 @@ data class MessagesLoaded( val messages: List, val profiles: HashMap, val groups: HashMap -) : VKEvent() +) : VkEvent() data class MessagesMarkAsImportant( val messagesIds: List, val important: Boolean -) : VKEvent() \ No newline at end of file +) : VkEvent() + +data class MessagesPin( + val message: VkMessage +) : VkEvent() + +object MessagesUnpin : VkEvent() + 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 e0fad5f3..3140463a 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 @@ -9,7 +9,6 @@ import android.widget.Space import android.widget.TextView import androidx.appcompat.widget.LinearLayoutCompat import androidx.core.content.ContextCompat -import androidx.core.view.isInvisible import androidx.core.view.isVisible import coil.load import com.meloda.fast.R @@ -65,14 +64,17 @@ class MessagesPreparator constructor( ContextCompat.getDrawable(context, R.drawable.ic_message_out_background) private val backgroundMiddleOut = ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle) -// private val backgroundStrokeOut = -// ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_stroke) -// private val backgroundMiddleStrokeOut = -// ContextCompat.getDrawable(context, R.drawable.ic_message_out_background_middle_stroke) private val rootHighlightedColor = ContextCompat.getColor(context, R.color.n2_100) + private var photoClickListener: ((url: String) -> Unit)? = null + + fun setPhotoClickListener(unit: ((url: String) -> Unit)?): MessagesPreparator { + this.photoClickListener = unit + return this + } + fun prepare() { val messageUser: VkUser? = (if (message.isUser()) { profiles[message.fromId] @@ -104,20 +106,24 @@ class MessagesPreparator constructor( ) if (message.isPeerChat()) { - - val fromDiffSender = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message) + val prevSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(prevMessage, message) + val nextSenderDiff = VkUtils.isPreviousMessageFromDifferentSender(message, nextMessage) val fiveMinAgo = VkUtils.isPreviousMessageSentFiveMinutesAgo(prevMessage, message) val change = (prevMessage?.date ?: 0) - message.date Log.d( "Fast::MessagesPreparator", - "text: ${message.text}; prevText: ${prevMessage?.text}; time change: $change; fromDiffSender: $fromDiffSender; fiveMinAgo: $fiveMinAgo; " + "text: ${message.text}; prevText: ${prevMessage?.text}; time change: $change; fromDiffSender: $prevSenderDiff; fiveMinAgo: $fiveMinAgo; " ) - title?.isVisible = fromDiffSender || fiveMinAgo + title?.isVisible = prevSenderDiff || fiveMinAgo - avatar?.isInvisible = fromDiffSender && fiveMinAgo + avatar?.visibility = + if (nextSenderDiff + || (fiveMinAgo && prevSenderDiff) + || (!prevSenderDiff && nextMessage == null) + ) View.VISIBLE else View.INVISIBLE } else { title?.isVisible = false avatar?.isVisible = false @@ -131,7 +137,6 @@ class MessagesPreparator constructor( } title.text = titleString - title.measure(0, 0) } } @@ -164,13 +169,16 @@ class MessagesPreparator constructor( attachmentContainer.removeAllViews() } else { attachmentContainer.isVisible = true + AttachmentInflater( context = context, container = attachmentContainer, message = message, groups = groups, profiles = profiles - ).inflate() + ) + .setPhotoClickListener(photoClickListener) + .inflate() } } } diff --git a/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt new file mode 100644 index 00000000..f3f99443 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewFragment.kt @@ -0,0 +1,48 @@ +package com.meloda.fast.screens.photos + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.fragment.app.viewModels +import com.meloda.fast.base.BaseViewModelFragment +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class PhotoViewFragment : BaseViewModelFragment() { + + override val viewModel: PhotoViewViewModel by viewModels() + +// private val photosList: MutableList = mutableListOf() + + private var photoLink: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + photoLink = requireArguments().getString("photoLink") + +// val list: List<*>? = Gson().fromJson( +// requireArguments().getString("photosList"), +// List::class.java +// ) +// +// list?.forEach { if (it is VkPhoto) photosList.add(it) } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return ImageView(requireContext()) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + photoLink?.let { viewModel.loadImageFromUrl(it, requireView() as ImageView) } + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt new file mode 100644 index 00000000..b9a2984d --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/screens/photos/PhotoViewViewModel.kt @@ -0,0 +1,22 @@ +package com.meloda.fast.screens.photos + +import android.widget.ImageView +import androidx.lifecycle.viewModelScope +import coil.load +import com.meloda.fast.base.viewmodel.BaseViewModel +import kotlinx.coroutines.launch + +class PhotoViewViewModel : BaseViewModel() { + + fun loadImageFromUrl( + url: String, + imageView: ImageView + ) = viewModelScope.launch { + imageView.load(url) + } + + fun saveImageToLocalStorage(url: String) = viewModelScope.launch { + TODO("Not implemented") + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt index f300bbe2..f934a9e4 100644 --- a/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt +++ b/app/src/main/kotlin/com/meloda/fast/util/TimeUtils.kt @@ -7,6 +7,8 @@ import java.util.* object TimeUtils { + const val ONE_DAY_IN_SECONDS = 86400 + fun removeTime(date: Date): Long { return Calendar.getInstance().apply { time = date diff --git a/app/src/main/res/drawable/ic_round_done_24.xml b/app/src/main/res/drawable/ic_round_done_24.xml new file mode 100644 index 00000000..2231d757 --- /dev/null +++ b/app/src/main/res/drawable/ic_round_done_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/fragment_messages_history.xml b/app/src/main/res/layout/fragment_messages_history.xml index 0dddcff8..08338bec 100644 --- a/app/src/main/res/layout/fragment_messages_history.xml +++ b/app/src/main/res/layout/fragment_messages_history.xml @@ -219,8 +219,10 @@ android:minHeight="105dp" android:orientation="vertical" android:padding="16dp" + android:visibility="gone" app:layout_anchor="@+id/messagePanel" - app:layout_anchorGravity="center_vertical|top"> + app:layout_anchorGravity="center_vertical|top" + tools:visibility="visible"> + android:tint="@color/n1_800" /> diff --git a/app/src/main/res/layout/item_message_in.xml b/app/src/main/res/layout/item_message_in.xml index f54f2d22..a142f1e8 100644 --- a/app/src/main/res/layout/item_message_in.xml +++ b/app/src/main/res/layout/item_message_in.xml @@ -70,19 +70,6 @@ android:textColor="@color/n1_800" tools:text="This" /> - - - - @@ -96,6 +83,20 @@ 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 0b46e0e2..6b836505 100644 --- a/app/src/main/res/layout/item_message_out.xml +++ b/app/src/main/res/layout/item_message_out.xml @@ -1,6 +1,5 @@ - + android:layout_height="wrap_content" + android:layout_gravity="center_vertical|start" + android:padding="15dp" + android:textColor="@color/n1_900" + tools:text="This is test" /> - - - - - - - + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/messages.xml b/app/src/main/res/navigation/messages.xml index 8db6e5ac..911ca742 100644 --- a/app/src/main/res/navigation/messages.xml +++ b/app/src/main/res/navigation/messages.xml @@ -21,6 +21,17 @@ android:id="@+id/messagesHistoryFragment" android:name="com.meloda.fast.screens.messages.MessagesHistoryFragment" android:label="MessagesHistoryFragment" - tools:layout="@layout/fragment_messages_history" /> + tools:layout="@layout/fragment_messages_history"> + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 04e57b21..d51307e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -117,4 +117,8 @@ Reply Mark as important Unmark as important + Time: %s + Pin + Unpin + Edit From 074400daab401171a106169bc25427670d212a3d Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sun, 10 Oct 2021 00:40:30 +0300 Subject: [PATCH 04/15] delete dialogs delete messages (+ for all & mark as spam) --- .../com/meloda/fast/api/ApiExtensions.kt | 8 ++ .../kotlin/com/meloda/fast/api/VkUtils.kt | 59 ++++++++++++ .../meloda/fast/api/model/VkConversation.kt | 11 +++ .../api/model/request/ConversationsRequest.kt | 5 ++ .../fast/api/model/request/MessagesRequest.kt | 50 ++++++++--- .../com/meloda/fast/api/network/VkUrls.kt | 6 +- .../datasource/ConversationsDataSource.kt | 9 +- .../network/datasource/MessagesDataSource.kt | 3 + .../api/network/repo/ConversationsRepo.kt | 8 +- .../fast/api/network/repo/MessagesRepo.kt | 4 + .../conversations/ConversationsAdapter.kt | 47 +++++----- .../conversations/ConversationsFragment.kt | 38 +++++++- .../conversations/ConversationsViewModel.kt | 21 +++-- .../messages/MessagesHistoryAdapter.kt | 56 ++++++++---- .../messages/MessagesHistoryFragment.kt | 90 ++++++++++++++----- .../messages/MessagesHistoryViewModel.kt | 24 +++++ .../screens/messages/MessagesPreparator.kt | 15 +--- .../main/res/layout/dialog_message_delete.xml | 22 +++++ app/src/main/res/values/strings.xml | 11 +++ 19 files changed, 392 insertions(+), 95 deletions(-) create mode 100644 app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt create mode 100644 app/src/main/res/layout/dialog_message_delete.xml diff --git a/app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt b/app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt new file mode 100644 index 00000000..95b757b1 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt @@ -0,0 +1,8 @@ +package com.meloda.fast.api + +object ApiExtensions { + + val Boolean.intString get() = (if (this) 1 else 0).toString() + + +} \ No newline at end of file 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 d2b4d302..070a90f9 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt @@ -7,6 +7,7 @@ import android.text.SpannableString import android.text.style.StyleSpan import androidx.core.content.ContextCompat import com.meloda.fast.R +import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkUser @@ -16,6 +17,64 @@ import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem object VkUtils { + fun getMessageUser(message: VkMessage, profiles: Map): VkUser? { + return (if (!message.isUser()) null + else profiles[message.fromId]).also { message.user.value = it } + } + + fun getMessageGroup(message: VkMessage, groups: Map): VkGroup? { + return (if (!message.isGroup()) null + else groups[message.fromId]).also { message.group.value = it } + } + + fun getMessageAvatar( + message: VkMessage, + messageUser: VkUser?, + messageGroup: VkGroup? + ): String? { + return when { + message.isUser() -> messageUser?.photo200 + message.isGroup() -> messageGroup?.photo200 + else -> null + } + } + + fun getMessageTitle( + message: VkMessage, + messageUser: VkUser?, + messageGroup: VkGroup? + ): String? { + return when { + message.isUser() -> messageUser?.fullName + message.isGroup() -> messageGroup?.name + else -> null + } + } + + fun getConversationUser(conversation: VkConversation, profiles: Map): VkUser? { + return (if (!conversation.isUser()) null + else profiles[conversation.id]).also { conversation.user.value = it } + } + + fun getConversationGroup(conversation: VkConversation, groups: Map): VkGroup? { + return (if (!conversation.isGroup()) null + else groups[conversation.id]).also { conversation.group.value = it } + } + + fun getConversationAvatar( + conversation: VkConversation, + conversationUser: VkUser?, + conversationGroup: VkGroup? + ): String? { + return when { + conversation.ownerId == VKConstants.FAST_GROUP_ID -> null + conversation.isUser() -> conversationUser?.photo200 + conversation.isGroup() -> conversationGroup?.photo200 + conversation.isChat() -> conversation.photo200 + else -> null + } + } + fun prepareMessageText(text: String, forConversations: Boolean? = null): String { return text.apply { if (forConversations == true) replace("\n", "") diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt b/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt index 76ca6d6f..c6673448 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/VkConversation.kt @@ -1,9 +1,12 @@ package com.meloda.fast.api.model import android.os.Parcelable +import androidx.lifecycle.MutableLiveData import androidx.room.Embedded import androidx.room.Entity +import androidx.room.Ignore import androidx.room.PrimaryKey +import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Entity(tableName = "conversations") @@ -34,6 +37,14 @@ data class VkConversation( var lastMessage: VkMessage? = null, ) : Parcelable { + @Ignore + @IgnoredOnParcel + val user = MutableLiveData() + + @Ignore + @IgnoredOnParcel + val group = MutableLiveData() + fun isChat() = type == "chat" fun isUser() = type == "user" fun isGroup() = type == "group" diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/request/ConversationsRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/model/request/ConversationsRequest.kt index d5a1e66e..16146454 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/request/ConversationsRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/request/ConversationsRequest.kt @@ -23,4 +23,9 @@ data class ConversationsGetRequest( extended?.let { this["extended"] = it.toString() } startMessageId?.let { this["start_message_id"] = it.toString() } } +} + +@Parcelize +data class ConversationsDeleteRequest(val peerId: Int) : Parcelable { + val map get() = mapOf("peer_id" to peerId.toString()) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt index 1cb01f21..10d9bc94 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.model.request import android.os.Parcelable +import com.meloda.fast.api.ApiExtensions.intString import kotlinx.parcelize.Parcelize @Parcelize @@ -20,9 +21,9 @@ data class MessagesGetHistoryRequest( ).apply { count?.let { this["count"] = it.toString() } offset?.let { this["offset"] = it.toString() } - extended?.let { this["extended"] = (if (it) 1 else 0).toString() } + extended?.let { this["extended"] = it.intString } startMessageId?.let { this["start_message_id"] = it.toString() } - rev?.let { this["rev"] = (if (it) 1 else 0).toString() } + rev?.let { this["rev"] = it.intString } fields?.let { this["fields"] = it } } @@ -51,8 +52,8 @@ data class MessagesSendRequest( lon?.let { this["lon"] = it.toString() } replyTo?.let { this["reply_to"] = it.toString() } stickerId?.let { this["sticker_id"] = it.toString() } - disableMentions?.let { this["disable_mentions"] = (if (it) 1 else 0).toString() } - dontParseLinks?.let { this["dont_parse_links"] = (if (it) 1 else 0).toString() } + disableMentions?.let { this["disable_mentions"] = it.intString } + dontParseLinks?.let { this["dont_parse_links"] = it.intString } } } @@ -65,11 +66,25 @@ data class MessagesMarkAsImportantRequest( val map get() = mutableMapOf( "message_ids" to messagesIds.joinToString { it.toString() }, - "important" to (if (important) 1 else 0).toString() + "important" to important.intString ) } +@Parcelize +data class MessagesGetLongPollServerRequest( + val needPts: Boolean, + val version: Int +) : Parcelable { + + val map + get() = mutableMapOf( + "need_pts" to needPts.intString, + "version" to version.toString() + ) +} + + @Parcelize data class MessagesPinMessageRequest( val peerId: Int, @@ -93,14 +108,27 @@ data class MessagesUnPinMessageRequest(val peerId: Int) : Parcelable { } @Parcelize -data class MessagesGetLongPollServerRequest( - val needPts: Boolean, - val version: Int +data class MessagesDeleteRequest( + val peerId: Int, + val messagesIds: List? = null, + val conversationsMessagesIds: List? = null, + val isSpam: Boolean? = null, + val deleteForAll: Boolean? = null ) : Parcelable { val map get() = mutableMapOf( - "need_pts" to (if (needPts) 1 else 0).toString(), - "version" to version.toString() - ) + "peer_id" to peerId.toString() + ).apply { + isSpam?.let { this["spam"] = it.intString } + deleteForAll?.let { this["delete_for_all"] = it.intString } + messagesIds?.let { + this["message_ids"] = it.joinToString { id -> id.toString() } + } + + conversationsMessagesIds?.let { + this["conversation_message_ids"] = it.joinToString { id -> id.toString() } + } + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt index ea4f2c5f..25f0d8fa 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt @@ -12,6 +12,7 @@ object VkUrls { object Conversations { const val Get = "$API/messages.getConversations" + const val Delete = "$API/messages.deleteConversation" } object Users { @@ -22,10 +23,11 @@ object VkUrls { const val GetHistory = "$API/messages.getHistory" const val Send = "$API/messages.send" const val MarkAsImportant = "$API/messages.markAsImportant" - const val Pin = "$API/messages.pin" - const val Unpin = "$API/messages.unpin" const val GetLongPollServer = "$API/messages.getLongPollServer" const val GetLongPollHistory = "$API/messages.getLongPollHistory" + const val Pin = "$API/messages.pin" + const val Unpin = "$API/messages.unpin" + const val Delete = "$API/messages.delete" } diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/ConversationsDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/ConversationsDataSource.kt index 198bf9b3..92ad82f7 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/ConversationsDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/ConversationsDataSource.kt @@ -1,8 +1,9 @@ package com.meloda.fast.api.network.datasource import com.meloda.fast.api.model.VkConversation -import com.meloda.fast.api.network.repo.ConversationsRepo +import com.meloda.fast.api.model.request.ConversationsDeleteRequest import com.meloda.fast.api.model.request.ConversationsGetRequest +import com.meloda.fast.api.network.repo.ConversationsRepo import com.meloda.fast.database.dao.ConversationsDao import javax.inject.Inject @@ -11,8 +12,10 @@ class ConversationsDataSource @Inject constructor( private val dao: ConversationsDao ) { - suspend fun getAllChats(params: ConversationsGetRequest) = repo.getAllChats(params.map) + suspend fun get(params: ConversationsGetRequest) = repo.get(params.map) - suspend fun storeConversations(conversations: List) = dao.insert(conversations) + suspend fun delete(params: ConversationsDeleteRequest) = repo.delete(params.map) + + suspend fun store(conversations: List) = dao.insert(conversations) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt index 472004da..8827d0ce 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt @@ -29,6 +29,9 @@ class MessagesDataSource @Inject constructor( suspend fun unpin(params: MessagesUnPinMessageRequest) = repo.unpin(params.map) + suspend fun delete(params: MessagesDeleteRequest) = + repo.delete(params.map) + suspend fun store(messages: List) = dao.insert(messages) suspend fun getCached(peerId: Int) = dao.getByPeerId(peerId) diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt index b394e85a..21a29a29 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt @@ -1,9 +1,9 @@ package com.meloda.fast.api.network.repo import com.meloda.fast.api.base.ApiResponse +import com.meloda.fast.api.model.response.ConversationsGetResponse import com.meloda.fast.api.network.Answer import com.meloda.fast.api.network.VkUrls -import com.meloda.fast.api.model.response.ConversationsGetResponse import retrofit2.http.FieldMap import retrofit2.http.FormUrlEncoded import retrofit2.http.POST @@ -12,6 +12,10 @@ interface ConversationsRepo { @FormUrlEncoded @POST(VkUrls.Conversations.Get) - suspend fun getAllChats(@FieldMap params: Map): Answer> + suspend fun get(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Conversations.Delete) + suspend fun delete(@FieldMap params: Map): Answer> } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt index 1b4c6717..79c96f88 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt @@ -36,4 +36,8 @@ interface MessagesRepo { @POST(VkUrls.Messages.Unpin) suspend fun unpin(@FieldMap params: Map): Answer> + @FormUrlEncoded + @POST(VkUrls.Messages.Delete) + suspend fun delete(@FieldMap params: Map): Answer> + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsAdapter.kt index 2f829598..ff80af05 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsAdapter.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsAdapter.kt @@ -75,29 +75,17 @@ class ConversationsAdapter constructor( return } - val chatUser: VkUser? = if (conversation.isUser()) { - profiles[conversation.id] - } else null + val conversationUser = VkUtils.getConversationUser(conversation, profiles) + val conversationGroup = VkUtils.getConversationGroup(conversation, groups) - val messageUser: VkUser? = if (message.isUser()) { - profiles[message.fromId] - } else null + val messageUser = VkUtils.getMessageUser(message, profiles) + val messageGroup = VkUtils.getMessageGroup(message, groups) - val chatGroup: VkGroup? = if (conversation.isGroup()) { - groups[conversation.id] - } else null - - val messageGroup: VkGroup? = if (message.isGroup()) { - groups[message.fromId] - } else null - - val avatar = when { - conversation.ownerId == VKConstants.FAST_GROUP_ID -> null - conversation.isUser() && chatUser != null && !chatUser.photo200.isNullOrBlank() -> chatUser.photo200 - conversation.isGroup() && chatGroup != null && !chatGroup.photo200.isNullOrBlank() -> chatGroup.photo200 - conversation.isChat() && !conversation.photo200.isNullOrBlank() -> conversation.photo200 - else -> null - } + val avatar = VkUtils.getConversationAvatar( + conversation = conversation, + conversationUser = conversationUser, + conversationGroup = conversationGroup + ) binding.avatar.isVisible = avatar != null @@ -136,7 +124,7 @@ class ConversationsAdapter constructor( } } - binding.online.isVisible = chatUser?.online == true + binding.online.isVisible = conversationUser?.online == true binding.pin.isVisible = conversation.isPinned @@ -210,7 +198,8 @@ class ConversationsAdapter constructor( binding.message.text = spanMessage binding.title.text = - getItem(position).title ?: chatUser?.toString() ?: chatGroup?.name ?: "..." + getItem(position).title ?: conversationUser?.toString() ?: conversationGroup?.name + ?: "..." binding.date.text = TimeUtils.getLocalizedTime(context, message.date * 1000L) @@ -232,6 +221,18 @@ class ConversationsAdapter constructor( } } + fun removeConversation(conversationId: Int): Int? { + for (i in values.indices) { + val conversation = values[i] + if (conversation.id == conversationId) { + values.removeAt(i) + return i + } + } + + return null + } + companion object { private val COMPARATOR = object : DiffUtil.ItemCallback() { override fun areItemsTheSame( diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt index 0071d184..bb37e14c 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 @@ -188,9 +188,11 @@ class ConversationsFragment : override fun onEvent(event: VkEvent) { super.onEvent(event) when (event) { - is ConversationsLoaded -> refreshConversations(event) is StartProgressEvent -> onProgressStarted() is StopProgressEvent -> onProgressStopped() + + is ConversationsLoaded -> refreshConversations(event) + is ConversationsDelete -> deleteConversation(event.peerId) } } @@ -263,8 +265,40 @@ class ConversationsFragment : } private fun onItemLongClick(position: Int): Boolean { - binding.createChat.performClick() + showOptionsDialog(position) return true } + private fun showOptionsDialog(position: Int) { + val conversation = adapter[position] + + val delete = getString(R.string.conversation_context_action_delete) + + val params = mutableListOf(delete) + + val arrayParams = params.toTypedArray() + + MaterialAlertDialogBuilder(requireContext()) + .setItems(arrayParams) { _, which -> + when (params[which]) { + delete -> showDeleteConversationDialog(conversation.id) + } + }.show() + } + + private fun showDeleteConversationDialog(conversationId: Int) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.confirm_delete_conversation) + .setPositiveButton(R.string.action_delete) { _, _ -> + viewModel.deleteConversation(conversationId) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun deleteConversation(conversationId: Int) { + val index = adapter.removeConversation(conversationId) ?: return + adapter.notifyItemRemoved(index) + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt index 83e8906b..f3bd08db 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 @@ -6,6 +6,7 @@ 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.VkUser +import com.meloda.fast.api.model.request.ConversationsDeleteRequest import com.meloda.fast.api.model.request.ConversationsGetRequest import com.meloda.fast.api.model.request.UsersGetRequest import com.meloda.fast.api.network.datasource.ConversationsDataSource @@ -20,15 +21,15 @@ import javax.inject.Inject @HiltViewModel class ConversationsViewModel @Inject constructor( - private val dataSource: ConversationsDataSource, - private val usersDataSource: UsersDataSource + private val conversations: ConversationsDataSource, + private val users: UsersDataSource ) : BaseViewModel() { fun loadConversations( offset: Int? = null ) = viewModelScope.launch(Dispatchers.Default) { makeJob({ - dataSource.getAllChats( + conversations.get( ConversationsGetRequest( count = 30, extended = true, @@ -69,16 +70,24 @@ class ConversationsViewModel @Inject constructor( } fun loadProfileUser() = viewModelScope.launch { - makeJob({ usersDataSource.getById(UsersGetRequest(fields = VKConstants.USER_FIELDS)) }, + makeJob({ users.getById(UsersGetRequest(fields = VKConstants.USER_FIELDS)) }, onAnswer = { it.response?.let { r -> val users = r.map { u -> u.asVkUser() } - usersDataSource.storeUsers(users) + this@ConversationsViewModel.users.storeUsers(users) UserConfig.vkUser.value = users[0] } }) } + + fun deleteConversation(peerId: Int) = viewModelScope.launch { + makeJob({ + conversations.delete( + ConversationsDeleteRequest(peerId) + ) + }, onAnswer = { sendEvent(ConversationsDelete(peerId)) }) + } } data class ConversationsLoaded( @@ -89,3 +98,5 @@ data class ConversationsLoaded( val profiles: HashMap, val groups: HashMap ) : VkEvent() + +data class ConversationsDelete(val peerId: Int) : VkEvent() \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt index 594ef13a..29c0b9ce 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,7 +5,6 @@ import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.view.View import android.view.ViewGroup -import android.widget.AdapterView import android.widget.Toast import androidx.appcompat.widget.LinearLayoutCompat import androidx.core.view.isVisible @@ -17,7 +16,6 @@ import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.attachments.VkAttachment import com.meloda.fast.api.model.attachments.VkPhoto import com.meloda.fast.base.adapter.BaseAdapter import com.meloda.fast.base.adapter.BaseHolder @@ -34,11 +32,7 @@ class MessagesHistoryAdapter constructor( val groups: HashMap = hashMapOf() ) : BaseAdapter(context, values, COMPARATOR) { - private var highlightTimer: Timer? = null - - var onItemClickListener: ((position: Int, view: View) -> Unit)? = null - - var attachmentClickListener: ((attachment: VkAttachment) -> Unit)? = null + var avatarLongClickListener: ((position: Int) -> Unit)? = null override fun getItemViewType(position: Int): Int { when { @@ -76,11 +70,19 @@ class MessagesHistoryAdapter constructor( } } - override fun initListeners(itemView: View, position: Int) { - if (itemView is AdapterView<*>) return +// override fun initListeners(itemView: View, position: Int) { +// if (itemView is AdapterView<*>) return +// +// itemView.setOnClickListener { onItemClickListener?.invoke(position, itemView) } +// itemView.setOnLongClickListener { itemLongClickListener.invoke(position) } +// } - itemView.setOnClickListener { onItemClickListener?.invoke(position, itemView) } - itemView.setOnLongClickListener { itemLongClickListener.invoke(position) } + + val actualSize get() = values.size + + override fun getItemCount(): Int { + if (actualSize == 0) return 2 + return super.getItemCount() + 2 } private fun createEmptyView(size: Int) = View(context).apply { @@ -142,6 +144,11 @@ class MessagesHistoryAdapter constructor( ).setPhotoClickListener { Toast.makeText(context, "Photo url: $it", Toast.LENGTH_LONG).show() }.prepare() + + binding.avatar.setOnLongClickListener() { + avatarLongClickListener?.invoke(position) + true + } } } @@ -230,11 +237,30 @@ class MessagesHistoryAdapter constructor( } } - val actualSize get() = values.size + fun removeMessageById(id: Int): Int? { + for (i in values.indices) { + val message = values[i] + if (message.id == id) { + values.removeAt(i) + return i + } + } - override fun getItemCount(): Int { - if (actualSize == 0) return 2 - return super.getItemCount() + 2 + return null + } + + fun removeMessagesByIds(ids: List): List { + val positions = mutableListOf() + + for (i in values.indices) { + val message = values[i] + if (ids.contains(message.id)) { + values.removeAt(i) + positions += i + } + } + + return positions } companion object { diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt index fafca889..7009e100 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 @@ -6,6 +6,7 @@ import android.os.Bundle import android.text.TextUtils import android.view.View import android.viewbinding.library.fragment.viewBinding +import android.widget.Toast import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.core.content.ContextCompat import androidx.core.view.isVisible @@ -20,6 +21,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.meloda.fast.R import com.meloda.fast.api.UserConfig import com.meloda.fast.api.VKConstants +import com.meloda.fast.api.VkUtils import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkMessage @@ -28,6 +30,7 @@ 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.databinding.DialogMessageDeleteBinding import com.meloda.fast.databinding.FragmentMessagesHistoryBinding import com.meloda.fast.extensions.TextViewExtensions.clear import com.meloda.fast.util.AndroidUtils @@ -65,8 +68,9 @@ class MessagesHistoryFragment : private val adapter: MessagesHistoryAdapter by lazy { MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also { - it.onItemClickListener = this::onItemClick + it.itemClickListener = this::onItemClick it.itemLongClickListener = this::onItemLongClick + it.avatarLongClickListener = this::onAvatarLongClickListener } } @@ -316,11 +320,14 @@ class MessagesHistoryFragment : super.onEvent(event) when (event) { + is StartProgressEvent -> onProgressStarted() + is StopProgressEvent -> onProgressStopped() + is MessagesMarkAsImportant -> markMessagesAsImportant(event) is MessagesLoaded -> refreshMessages(event) is MessagesPin -> conversation.pinnedMessage = event.message - is StartProgressEvent -> onProgressStarted() - is StopProgressEvent -> onProgressStopped() + is MessagesUnpin -> conversation.pinnedMessage = null + is MessagesDelete -> deleteMessages(event) } } @@ -402,7 +409,23 @@ class MessagesHistoryFragment : else binding.recyclerView.scrollToPosition(adapter.lastPosition) } - private fun onItemClick(position: Int, view: View) { + private fun onItemClick(position: Int) { + showOptionsDialog(position) + } + + private fun onItemLongClick(position: Int) = true + + private fun onAvatarLongClickListener(position: Int) { + val message = adapter.values[position] + + val messageUser = VkUtils.getMessageUser(message, adapter.profiles) + val messageGroup = VkUtils.getMessageGroup(message, adapter.groups) + + val title = VkUtils.getMessageTitle(message, messageUser, messageGroup) + Toast.makeText(requireContext(), title, Toast.LENGTH_SHORT).show() + } + + private fun showOptionsDialog(position: Int) { val message = adapter.values[position] if (message.action != null) return @@ -414,6 +437,11 @@ class MessagesHistoryFragment : ).format(message.date * 1000L) ) + val important = getString( + if (message.important) R.string.message_context_action_unmark_as_important + else R.string.message_context_action_mark_as_important + ) + val reply = getString(R.string.message_context_action_reply) val isMessageAlreadyPinned = message.id == conversation.pinnedMessage?.id @@ -425,27 +453,25 @@ class MessagesHistoryFragment : val edit = getString(R.string.message_context_action_edit) - val important = getString( - if (message.important) R.string.message_context_action_unmark_as_important - else R.string.message_context_action_mark_as_important + val delete = getString(R.string.message_context_action_delete) + + val params = mutableListOf( + important, reply ) - val params = mutableListOf() - params.add(reply) - if (conversation.canChangePin) { - params.add(pin) + params += pin } if (message.canEdit()) { - params.add(edit) + params += edit } - params.add(important) + params += delete val arrayParams = params.toTypedArray() - val dialog = MaterialAlertDialogBuilder(requireContext()) + MaterialAlertDialogBuilder(requireContext()) .setTitle(time) .setItems(arrayParams) { _, which -> when (params[which]) { @@ -468,16 +494,40 @@ class MessagesHistoryFragment : if (attachmentController.message.value != message) attachmentController.message.value = message } + delete -> showDeleteMessageDialog(message) } - } - - dialog.show() - + }.show() } - private fun onItemLongClick(position: Int): Boolean { + private fun showDeleteMessageDialog(message: VkMessage) { + val binding = DialogMessageDeleteBinding.inflate(layoutInflater, null, false) - return true + binding.check.setText( + if (message.isOut) R.string.message_delete_for_all + else R.string.message_mark_as_spam + ) + + binding.check.isEnabled = !message.isOut || message.canEdit() + + MaterialAlertDialogBuilder(requireContext()) + .setTitle(R.string.confirm_delete_message) + .setView(binding.root) + .setPositiveButton(R.string.action_delete) { _, _ -> + viewModel.deleteMessage( + peerId = conversation.id, + messagesIds = listOf(message.id), + isSpam = if (message.isOut) null else binding.check.isChecked, + deleteForAll = if (!message.isOut || !message.canEdit()) null else binding.check.isChecked + ) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun deleteMessages(event: MessagesDelete) { + adapter.removeMessagesByIds(event.messagesIds).let { + it.forEach { index -> adapter.notifyItemRemoved(index) } + } } private inner class AttachmentPanelController { 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 f425a01b..2295ffcd 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 @@ -154,6 +154,26 @@ class MessagesHistoryViewModel @Inject constructor( ) } } + + fun deleteMessage( + peerId: Int, + messagesIds: List? = null, + conversationsMessagesIds: List? = null, + isSpam: Boolean? = null, + deleteForAll: Boolean? = null + ) = viewModelScope.launch { + makeJob({ + messages.delete( + MessagesDeleteRequest( + peerId = peerId, + messagesIds = messagesIds, + conversationsMessagesIds = conversationsMessagesIds, + isSpam = isSpam, + deleteForAll = deleteForAll + ) + ) + }, onAnswer = { sendEvent(MessagesDelete(messagesIds = messagesIds ?: listOf())) }) + } } data class MessagesLoaded( @@ -175,3 +195,7 @@ data class MessagesPin( object MessagesUnpin : VkEvent() +data class MessagesDelete( + val messagesIds: List +) : VkEvent() + 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 3140463a..c46d9b9e 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 @@ -76,13 +76,8 @@ class MessagesPreparator constructor( } fun prepare() { - val messageUser: VkUser? = (if (message.isUser()) { - profiles[message.fromId] - } else null).also { message.user.value = it } - - val messageGroup: VkGroup? = (if (message.isGroup()) { - groups[message.fromId] - } else null).also { message.group.value = it } + val messageUser = VkUtils.getMessageUser(message, profiles) + val messageGroup = VkUtils.getMessageGroup(message, groups) prepareRootBackground() @@ -225,11 +220,7 @@ class MessagesPreparator constructor( messageGroup: VkGroup? = null ) { if (avatar != null) { - val avatarUrl = when { - message.isUser() && messageUser != null && !messageUser.photo200.isNullOrBlank() -> messageUser.photo200 - message.isGroup() && messageGroup != null && !messageGroup.photo200.isNullOrBlank() -> messageGroup.photo200 - else -> null - } + val avatarUrl = VkUtils.getMessageAvatar(message, messageUser, messageGroup) avatar.load(avatarUrl) { crossfade(100) } } diff --git a/app/src/main/res/layout/dialog_message_delete.xml b/app/src/main/res/layout/dialog_message_delete.xml new file mode 100644 index 00000000..f5ba3f90 --- /dev/null +++ b/app/src/main/res/layout/dialog_message_delete.xml @@ -0,0 +1,22 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d51307e0..be7ae7f1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -121,4 +121,15 @@ Pin Unpin Edit + Delete + + Delete the message? + + Delete for all + + Mark as spam + + Delete + Delete + Delete the conversation? From 3e0bf30b0f2014d57b1b614d343d5c252bda9a04 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sun, 10 Oct 2021 01:06:32 +0300 Subject: [PATCH 05/15] not easter egg --- .../conversations/ConversationsFragment.kt | 31 ++++++++++++++----- app/src/main/res/values/strings.xml | 4 ++- 2 files changed, 27 insertions(+), 8 deletions(-) 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 bb37e14c..358beb6d 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 @@ -31,6 +31,7 @@ import com.meloda.fast.common.dataStore import com.meloda.fast.databinding.FragmentConversationsBinding import com.meloda.fast.util.AndroidUtils import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -172,14 +173,30 @@ class ConversationsFragment : } private fun showLogOutDialog() { + val isEasterEgg = UserConfig.userId == UserConfig.userId + 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)) + .setTitle( + if (isEasterEgg) "Выйти внаружу?" + else getString(R.string.sign_out_confirm_title) + ) + .setMessage(R.string.sign_out_confirm) + .setPositiveButton( + if (isEasterEgg) "Выйти внаружу" + else getString(R.string.action_sign_out) + ) { _, _ -> + lifecycleScope.launch(Dispatchers.Default) { + UserConfig.clear() + AppGlobal.appDatabase.clearAllTables() + + requireActivity().finishAffinity() + requireActivity().startActivity( + Intent( + requireContext(), + MainActivity::class.java + ) + ) + } } .setNegativeButton(R.string.no, null) .show() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index be7ae7f1..1fabaf95 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -111,7 +111,7 @@ Story Log out Confirmation - Are you really want to log out? + Signing out will delete all data related to this account from this device. Continue? Yes No Reply @@ -132,4 +132,6 @@ Delete Delete Delete the conversation? + Sign out + Sign out From ff5d449b3bfeb7b193a02ca66033b8dd352c1969 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sun, 10 Oct 2021 17:27:04 +0300 Subject: [PATCH 06/15] fix avatar on conversations screen refactoring removing unused classes --- .../kotlin/com/meloda/fast/api/VKException.kt | 6 +- .../fast/api/model/response/UsersResponse.kt | 2 - .../com/meloda/fast/api/network/VkUrls.kt | 3 + .../{datasource => auth}/AuthDataSource.kt | 4 +- .../api/network/{repo => auth}/AuthRepo.kt | 10 +- .../request => network/auth}/AuthRequest.kt | 2 +- .../response => network/auth}/AuthResponse.kt | 2 +- .../ConversationsDataSource.kt | 7 +- .../ConversationsRepo.kt | 15 +- .../conversations}/ConversationsRequest.kt | 2 +- .../conversations}/ConversationsResponse.kt | 2 +- .../{repo => longpoll}/LongPollRepo.kt | 2 +- .../MessagesDataSource.kt | 4 +- .../{repo => messages}/MessagesRepo.kt | 3 +- .../messages}/MessagesRequest.kt | 2 +- .../messages}/MessagesResponse.kt | 2 +- .../{datasource => users}/UsersDataSource.kt | 4 +- .../api/network/{repo => users}/UsersRepo.kt | 2 +- .../request => network/users}/UsersRequest.kt | 2 +- .../fast/api/network/users/UsersResponse.kt | 2 + .../com/meloda/fast/di/NetworkModule.kt | 14 +- .../fast/extensions/ContextExtensions.kt | 37 ---- .../fast/extensions/DrawableExtensions.kt | 13 -- .../com/meloda/fast/extensions/Extensions.kt | 77 -------- .../meloda/fast/extensions/FloatExtensions.kt | 11 -- .../fast/extensions/LiveDataExtensions.kt | 116 ------------ .../fast/extensions/StringExtensions.kt | 11 -- .../fast/extensions/TextViewExtensions.kt | 8 +- .../com/meloda/fast/io/BytesOutputStream.kt | 10 - .../kotlin/com/meloda/fast/io/Charsets.kt | 12 -- .../kotlin/com/meloda/fast/io/EasyStreams.kt | 174 ------------------ .../kotlin/com/meloda/fast/io/FileStreams.kt | 96 ---------- .../conversations/ConversationsFragment.kt | 46 +---- .../conversations/ConversationsViewModel.kt | 12 +- .../fast/screens/login/LoginViewModel.kt | 4 +- .../messages/MessagesHistoryViewModel.kt | 5 +- .../meloda/fast/service/LongPollService.kt | 6 +- .../kotlin/com/meloda/fast/util/ArrayUtils.kt | 59 ------ .../kotlin/com/meloda/fast/util/ColorUtils.kt | 30 --- .../kotlin/com/meloda/fast/util/ImageUtils.kt | 55 ------ .../kotlin/com/meloda/fast/util/TextUtils.kt | 15 -- .../main/kotlin/com/meloda/fast/util/Utils.kt | 49 ----- .../com/meloda/fast/widget/NoItemsView.kt | 19 +- app/src/main/res/drawable/test.png | Bin 44277 -> 0 bytes .../res/layout/fragment_conversations.xml | 50 ++--- .../main/res/menu/fragment_conversations.xml | 5 + 46 files changed, 113 insertions(+), 899 deletions(-) delete mode 100644 app/src/main/kotlin/com/meloda/fast/api/model/response/UsersResponse.kt rename app/src/main/kotlin/com/meloda/fast/api/network/{datasource => auth}/AuthDataSource.kt (62%) rename app/src/main/kotlin/com/meloda/fast/api/network/{repo => auth}/AuthRepo.kt (65%) rename app/src/main/kotlin/com/meloda/fast/api/{model/request => network/auth}/AuthRequest.kt (97%) rename app/src/main/kotlin/com/meloda/fast/api/{model/response => network/auth}/AuthResponse.kt (94%) rename app/src/main/kotlin/com/meloda/fast/api/network/{datasource => conversations}/ConversationsDataSource.kt (68%) rename app/src/main/kotlin/com/meloda/fast/api/network/{repo => conversations}/ConversationsRepo.kt (53%) rename app/src/main/kotlin/com/meloda/fast/api/{model/request => network/conversations}/ConversationsRequest.kt (94%) rename app/src/main/kotlin/com/meloda/fast/api/{model/response => network/conversations}/ConversationsResponse.kt (93%) rename app/src/main/kotlin/com/meloda/fast/api/network/{repo => longpoll}/LongPollRepo.kt (90%) rename app/src/main/kotlin/com/meloda/fast/api/network/{datasource => messages}/MessagesDataSource.kt (88%) rename app/src/main/kotlin/com/meloda/fast/api/network/{repo => messages}/MessagesRepo.kt (92%) rename app/src/main/kotlin/com/meloda/fast/api/{model/request => network/messages}/MessagesRequest.kt (98%) rename app/src/main/kotlin/com/meloda/fast/api/{model/response => network/messages}/MessagesResponse.kt (92%) rename app/src/main/kotlin/com/meloda/fast/api/network/{datasource => users}/UsersDataSource.kt (70%) rename app/src/main/kotlin/com/meloda/fast/api/network/{repo => users}/UsersRepo.kt (91%) rename app/src/main/kotlin/com/meloda/fast/api/{model/request => network/users}/UsersRequest.kt (92%) create mode 100644 app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/extensions/ContextExtensions.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/extensions/DrawableExtensions.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/extensions/Extensions.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/extensions/FloatExtensions.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/extensions/LiveDataExtensions.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/extensions/StringExtensions.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/io/BytesOutputStream.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/io/Charsets.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/io/EasyStreams.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/io/FileStreams.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/util/ArrayUtils.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/util/ImageUtils.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/util/TextUtils.kt delete mode 100644 app/src/main/kotlin/com/meloda/fast/util/Utils.kt delete mode 100644 app/src/main/res/drawable/test.png diff --git a/app/src/main/kotlin/com/meloda/fast/api/VKException.kt b/app/src/main/kotlin/com/meloda/fast/api/VKException.kt index e1eb98a2..353c8520 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VKException.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VKException.kt @@ -8,11 +8,9 @@ open class VKException( var code: Int = -1, var description: String = "", var error: String -) : - IOException(description) { +) : IOException(description) { - var captcha: Pair? = null - var validationSid: String? = null + // TODO: 10-Oct-21 remove this var json: JSONObject? = null override fun toString(): String { diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/response/UsersResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/model/response/UsersResponse.kt deleted file mode 100644 index dede65e8..00000000 --- a/app/src/main/kotlin/com/meloda/fast/api/model/response/UsersResponse.kt +++ /dev/null @@ -1,2 +0,0 @@ -package com.meloda.fast.api.model.response - diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt index 25f0d8fa..60f8e3dd 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt @@ -13,6 +13,9 @@ object VkUrls { object Conversations { const val Get = "$API/messages.getConversations" const val Delete = "$API/messages.deleteConversation" + const val Pin = "$API/messages.pinConversation" + const val Unpin = "$API/messages.unpinConversation" + const val ReorderPinned = "$API/messages.reorderPinnedConversations" } object Users { diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthDataSource.kt similarity index 62% rename from app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthDataSource.kt index 0a1f63b6..49e9daaa 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/AuthDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthDataSource.kt @@ -1,7 +1,5 @@ -package com.meloda.fast.api.network.datasource +package com.meloda.fast.api.network.auth -import com.meloda.fast.api.network.repo.AuthRepo -import com.meloda.fast.api.model.request.RequestAuthDirect import javax.inject.Inject class AuthDataSource @Inject constructor( diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRepo.kt similarity index 65% rename from app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRepo.kt index e0b72cba..26d3c859 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/AuthRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRepo.kt @@ -1,10 +1,10 @@ -package com.meloda.fast.api.network.repo +package com.meloda.fast.api.network.auth -import com.meloda.fast.api.network.VkUrls -import com.meloda.fast.api.model.response.ResponseAuthDirect import com.meloda.fast.api.network.Answer -import com.meloda.fast.api.model.response.ResponseSendSms -import retrofit2.http.* +import com.meloda.fast.api.network.VkUrls +import retrofit2.http.GET +import retrofit2.http.Query +import retrofit2.http.QueryMap interface AuthRepo { diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/request/AuthRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRequest.kt similarity index 97% rename from app/src/main/kotlin/com/meloda/fast/api/model/request/AuthRequest.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRequest.kt index 6868f545..2226ba1b 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/request/AuthRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthRequest.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.request +package com.meloda.fast.api.network.auth import android.os.Parcelable import com.google.gson.annotations.SerializedName diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/response/AuthResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthResponse.kt similarity index 94% rename from app/src/main/kotlin/com/meloda/fast/api/model/response/AuthResponse.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthResponse.kt index 23e6c81f..2da17dee 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/response/AuthResponse.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/auth/AuthResponse.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.response +package com.meloda.fast.api.network.auth import android.os.Parcelable import com.google.gson.annotations.SerializedName diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/ConversationsDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsDataSource.kt similarity index 68% rename from app/src/main/kotlin/com/meloda/fast/api/network/datasource/ConversationsDataSource.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsDataSource.kt index 92ad82f7..515dd4b5 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/ConversationsDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsDataSource.kt @@ -1,9 +1,6 @@ -package com.meloda.fast.api.network.datasource +package com.meloda.fast.api.network.conversations import com.meloda.fast.api.model.VkConversation -import com.meloda.fast.api.model.request.ConversationsDeleteRequest -import com.meloda.fast.api.model.request.ConversationsGetRequest -import com.meloda.fast.api.network.repo.ConversationsRepo import com.meloda.fast.database.dao.ConversationsDao import javax.inject.Inject @@ -16,6 +13,8 @@ class ConversationsDataSource @Inject constructor( suspend fun delete(params: ConversationsDeleteRequest) = repo.delete(params.map) + + suspend fun store(conversations: List) = dao.insert(conversations) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRepo.kt similarity index 53% rename from app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRepo.kt index 21a29a29..1561e74d 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/ConversationsRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRepo.kt @@ -1,7 +1,6 @@ -package com.meloda.fast.api.network.repo +package com.meloda.fast.api.network.conversations import com.meloda.fast.api.base.ApiResponse -import com.meloda.fast.api.model.response.ConversationsGetResponse import com.meloda.fast.api.network.Answer import com.meloda.fast.api.network.VkUrls import retrofit2.http.FieldMap @@ -18,4 +17,16 @@ interface ConversationsRepo { @POST(VkUrls.Conversations.Delete) suspend fun delete(@FieldMap params: Map): Answer> + @FormUrlEncoded + @POST(VkUrls.Conversations.Pin) + suspend fun pin(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Conversations.Unpin) + suspend fun unpin(@FieldMap params: Map): Answer> + + @FormUrlEncoded + @POST(VkUrls.Conversations.ReorderPinned) + suspend fun reorderPinned(@FieldMap params: Map): Answer> + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/request/ConversationsRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRequest.kt similarity index 94% rename from app/src/main/kotlin/com/meloda/fast/api/model/request/ConversationsRequest.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRequest.kt index 16146454..81fdca5d 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/request/ConversationsRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRequest.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.request +package com.meloda.fast.api.network.conversations import android.os.Parcelable import kotlinx.parcelize.Parcelize diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/response/ConversationsResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsResponse.kt similarity index 93% rename from app/src/main/kotlin/com/meloda/fast/api/model/response/ConversationsResponse.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsResponse.kt index e3e1a821..c49d24e1 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/response/ConversationsResponse.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsResponse.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.response +package com.meloda.fast.api.network.conversations import android.os.Parcelable import com.google.gson.annotations.SerializedName diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/LongPollRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRepo.kt similarity index 90% rename from app/src/main/kotlin/com/meloda/fast/api/network/repo/LongPollRepo.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRepo.kt index 793f0940..997626d8 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/LongPollRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/longpoll/LongPollRepo.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.network.repo +package com.meloda.fast.api.network.longpoll import com.meloda.fast.api.base.ApiResponse import com.meloda.fast.api.network.Answer diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt similarity index 88% rename from app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt index 8827d0ce..479a9392 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/MessagesDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt @@ -1,8 +1,6 @@ -package com.meloda.fast.api.network.datasource +package com.meloda.fast.api.network.messages import com.meloda.fast.api.model.VkMessage -import com.meloda.fast.api.model.request.* -import com.meloda.fast.api.network.repo.MessagesRepo import com.meloda.fast.database.dao.MessagesDao import javax.inject.Inject diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt similarity index 92% rename from app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt index 79c96f88..4732ed19 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/MessagesRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt @@ -1,9 +1,8 @@ -package com.meloda.fast.api.network.repo +package com.meloda.fast.api.network.messages import com.meloda.fast.api.base.ApiResponse import com.meloda.fast.api.model.base.BaseVkLongPoll import com.meloda.fast.api.model.base.BaseVkMessage -import com.meloda.fast.api.model.response.MessagesGetHistoryResponse import com.meloda.fast.api.network.Answer import com.meloda.fast.api.network.VkUrls import retrofit2.http.FieldMap diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt similarity index 98% rename from app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt index 10d9bc94..a0e46a17 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/request/MessagesRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.request +package com.meloda.fast.api.network.messages import android.os.Parcelable import com.meloda.fast.api.ApiExtensions.intString diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/response/MessagesResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesResponse.kt similarity index 92% rename from app/src/main/kotlin/com/meloda/fast/api/model/response/MessagesResponse.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesResponse.kt index 9ddeeb24..1462c031 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/response/MessagesResponse.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesResponse.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.response +package com.meloda.fast.api.network.messages import android.os.Parcelable import com.meloda.fast.api.model.base.BaseVkConversation diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/UsersDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersDataSource.kt similarity index 70% rename from app/src/main/kotlin/com/meloda/fast/api/network/datasource/UsersDataSource.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/users/UsersDataSource.kt index 7e9939cf..86a7b88a 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/datasource/UsersDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersDataSource.kt @@ -1,8 +1,6 @@ -package com.meloda.fast.api.network.datasource +package com.meloda.fast.api.network.users import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.network.repo.UsersRepo -import com.meloda.fast.api.model.request.UsersGetRequest import com.meloda.fast.database.dao.UsersDao import javax.inject.Inject diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/repo/UsersRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersRepo.kt similarity index 91% rename from app/src/main/kotlin/com/meloda/fast/api/network/repo/UsersRepo.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/users/UsersRepo.kt index f11a408d..7db8e122 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/repo/UsersRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersRepo.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.network.repo +package com.meloda.fast.api.network.users import com.meloda.fast.api.base.ApiResponse import com.meloda.fast.api.model.base.BaseVkUser diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/request/UsersRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersRequest.kt similarity index 92% rename from app/src/main/kotlin/com/meloda/fast/api/model/request/UsersRequest.kt rename to app/src/main/kotlin/com/meloda/fast/api/network/users/UsersRequest.kt index afb9b2ab..5a88dec5 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/request/UsersRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersRequest.kt @@ -1,4 +1,4 @@ -package com.meloda.fast.api.model.request +package com.meloda.fast.api.network.users import android.os.Parcelable import kotlinx.parcelize.Parcelize diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt new file mode 100644 index 00000000..c4084e58 --- /dev/null +++ b/app/src/main/kotlin/com/meloda/fast/api/network/users/UsersResponse.kt @@ -0,0 +1,2 @@ +package com.meloda.fast.api.network.users + diff --git a/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt b/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt index 3ebaa503..989d3402 100644 --- a/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt +++ b/app/src/main/kotlin/com/meloda/fast/di/NetworkModule.kt @@ -4,11 +4,15 @@ import com.google.gson.Gson import com.google.gson.GsonBuilder import com.meloda.fast.api.network.AuthInterceptor import com.meloda.fast.api.network.ResultCallFactory -import com.meloda.fast.api.network.datasource.AuthDataSource -import com.meloda.fast.api.network.datasource.ConversationsDataSource -import com.meloda.fast.api.network.datasource.MessagesDataSource -import com.meloda.fast.api.network.datasource.UsersDataSource -import com.meloda.fast.api.network.repo.* +import com.meloda.fast.api.network.auth.AuthRepo +import com.meloda.fast.api.network.auth.AuthDataSource +import com.meloda.fast.api.network.conversations.ConversationsDataSource +import com.meloda.fast.api.network.conversations.ConversationsRepo +import com.meloda.fast.api.network.longpoll.LongPollRepo +import com.meloda.fast.api.network.messages.MessagesDataSource +import com.meloda.fast.api.network.users.UsersDataSource +import com.meloda.fast.api.network.messages.MessagesRepo +import com.meloda.fast.api.network.users.UsersRepo import com.meloda.fast.database.dao.ConversationsDao import com.meloda.fast.database.dao.MessagesDao import com.meloda.fast.database.dao.UsersDao diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/ContextExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/ContextExtensions.kt deleted file mode 100644 index dcce0bf6..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/ContextExtensions.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.meloda.fast.extensions - -import android.content.Context -import android.graphics.Typeface -import android.graphics.drawable.Drawable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.* -import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat - -object ContextExtensions { - - fun Context.drawable(@DrawableRes resId: Int): Drawable? { - return ContextCompat.getDrawable(this, resId) - } - - @ColorInt - fun Context.color(@ColorRes resId: Int): Int { - return ContextCompat.getColor(this, resId) - } - - fun Context.font(@FontRes resId: Int): Typeface? { - return ResourcesCompat.getFont(this, resId) - } - - fun Context.string(@StringRes resId: Int): String { - return getString(resId) - } - - fun Context.view(resId: Int, root: ViewGroup? = null, attachToRoot: Boolean = false): View { - return LayoutInflater.from(this).inflate(resId, root, attachToRoot) - } - - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/DrawableExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/DrawableExtensions.kt deleted file mode 100644 index 579e0442..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/DrawableExtensions.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.meloda.fast.extensions - -import android.graphics.drawable.Drawable -import androidx.annotation.ColorInt - -object DrawableExtensions { - - fun Drawable?.tint(@ColorInt color: Int): Drawable? { - this?.setTint(color) - return this - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/Extensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/Extensions.kt deleted file mode 100644 index 05ff43e3..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/Extensions.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.meloda.fast.extensions - -import android.graphics.* -import android.view.View -import androidx.core.view.isVisible -import kotlin.math.min - -fun Bitmap.borderedCircularBitmap( - borderColor: Int = 0, - borderWidth: Int = 0 -): Bitmap? { - val bitmap = Bitmap.createBitmap( - width, // width in pixels - height, // height in pixels - Bitmap.Config.ARGB_8888 - ) - - // canvas to draw circular bitmap - val canvas = Canvas(bitmap) - - // get the maximum radius - val radius = min(width / 2f, height / 2f) - - // create a path to draw circular bitmap border - val borderPath = Path().apply { - addCircle( - width / 2f, - height / 2f, - radius, - Path.Direction.CCW - ) - } - - // draw border on circular bitmap - canvas.clipPath(borderPath) - canvas.drawColor(borderColor) - - - // create a path for circular bitmap - val bitmapPath = Path().apply { - addCircle( - width / 2f, - height / 2f, - radius - borderWidth, - Path.Direction.CCW - ) - } - - canvas.clipPath(bitmapPath) - val paint = Paint().apply { - xfermode = PorterDuffXfermode(PorterDuff.Mode.CLEAR) - isAntiAlias = true - } - - // clear the circular bitmap drawing area - // it will keep bitmap transparency - canvas.drawBitmap(this, 0f, 0f, paint) - - // now draw the circular bitmap - canvas.drawBitmap(this, 0f, 0f, null) - - - val diameter = (radius * 2).toInt() - val x = (width - diameter) / 2 - val y = (height - diameter) / 2 - - // return cropped circular bitmap with border - return Bitmap.createBitmap( - bitmap, // source bitmap - x, // x coordinate of the first pixel in source - y, // y coordinate of the first pixel in source - diameter, // width - diameter // height - ) -} - -val View.isNotVisible get() = !isVisible \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/FloatExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/FloatExtensions.kt deleted file mode 100644 index 42fdca5e..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/FloatExtensions.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.extensions - -import kotlin.math.roundToInt - -object FloatExtensions { - - fun Float.int(): Int { - return roundToInt() - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/LiveDataExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/LiveDataExtensions.kt deleted file mode 100644 index 552855be..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/LiveDataExtensions.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.meloda.fast.extensions - -import androidx.annotation.UiThread -import androidx.lifecycle.MutableLiveData - -object LiveDataExtensions { - - operator fun MutableLiveData>.set(position: Int, v: T) { - val value = (this.value ?: arrayListOf()).apply { this[position] = v } - this.value = value - } - - operator fun MutableLiveData>.get(position: Int): T { - return (value as MutableList)[position] - } - - @JvmOverloads - fun MutableLiveData>.add(v: T, position: Int = -1) { - val value = (this.value ?: arrayListOf()).apply { - if (position == -1) this.add(v) else this.add(position, v) - } - - this.value = value - } - - @JvmOverloads - fun MutableLiveData>.addAll(values: List, position: Int = -1) { - val value = (this.value ?: arrayListOf()).apply { - if (position == -1) this.addAll(values) - else this.addAll(position, values) - } - - this.value = value - } - - @Suppress("TYPE_INFERENCE_ONLY_INPUT_TYPES_WARNING") - fun MutableLiveData>.removeAll(values: List) { - val value = (this.value ?: arrayListOf()).apply { - this.removeAll(values) - } - - this.value = value - } - - fun MutableLiveData>.removeAt(index: Int) { - val value = (this.value ?: arrayListOf()).apply { - this.removeAt(index) - } - - this.value = value - } - - fun MutableLiveData>.remove(item: T) { - val value = (this.value ?: arrayListOf()).apply { - this.remove(item) - } - - this.value = value - } - - operator fun MutableLiveData>.iterator(): Iterator { - return (value as MutableList).iterator() - } - - fun MutableLiveData>.clear() { - value = arrayListOf() - } - - val MutableLiveData>.indices get() = (value as MutableList).indices - - val MutableLiveData>.size get() = (value as MutableList).size - - fun MutableLiveData>.isEmpty(): Boolean { - return (value as MutableList).isEmpty() - } - - fun MutableLiveData>.isNotEmpty(): Boolean { - return !isEmpty() - } - - fun MutableLiveData>.requireValue() = value!! - - @UiThread - operator fun MutableLiveData>.plusAssign(values: List) { - val value = (this.value ?: arrayListOf()).apply { - this.addAll(values) - } - - this.value = value - } - - operator fun MutableLiveData>.plusAssign(v: T) { - val value = (this.value ?: arrayListOf()).apply { - this.add(v) - } - - this.value = value - } - - operator fun MutableLiveData>.minusAssign(values: List) { - val value = (this.value ?: arrayListOf()).apply { - this.removeAll(values) - } - - this.value = value - } - - operator fun MutableLiveData>.minusAssign(v: T) { - val value = (this.value ?: arrayListOf()).apply { - this.remove(v) - } - - this.value = value - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/StringExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/StringExtensions.kt deleted file mode 100644 index 7d2bde6a..00000000 --- a/app/src/main/kotlin/com/meloda/fast/extensions/StringExtensions.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.meloda.fast.extensions - -import java.util.* - -object StringExtensions { - - fun String.lowerCase(): String { - return toLowerCase(Locale.getDefault()) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/extensions/TextViewExtensions.kt b/app/src/main/kotlin/com/meloda/fast/extensions/TextViewExtensions.kt index 9d130722..b4a7ce18 100644 --- a/app/src/main/kotlin/com/meloda/fast/extensions/TextViewExtensions.kt +++ b/app/src/main/kotlin/com/meloda/fast/extensions/TextViewExtensions.kt @@ -1,17 +1,11 @@ package com.meloda.fast.extensions import android.widget.TextView -import com.google.android.material.textfield.TextInputLayout object TextViewExtensions { fun TextView.clear() { - text = "" + text = null } - fun TextInputLayout.clear() { - editText?.setText("") - } - - } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/io/BytesOutputStream.kt b/app/src/main/kotlin/com/meloda/fast/io/BytesOutputStream.kt deleted file mode 100644 index 65892969..00000000 --- a/app/src/main/kotlin/com/meloda/fast/io/BytesOutputStream.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.meloda.fast.io - -import java.io.ByteArrayOutputStream - -class BytesOutputStream : ByteArrayOutputStream { - constructor() : super(8192) - constructor(size: Int) : super(size) - - val byteArray: ByteArray = buf -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/io/Charsets.kt b/app/src/main/kotlin/com/meloda/fast/io/Charsets.kt deleted file mode 100644 index d9de4363..00000000 --- a/app/src/main/kotlin/com/meloda/fast/io/Charsets.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.meloda.fast.io - -import java.nio.charset.Charset -import java.nio.charset.StandardCharsets - -object Charsets { - - val ASCII: Charset = StandardCharsets.US_ASCII - - val UTF_8: Charset = StandardCharsets.UTF_8 - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/io/EasyStreams.kt b/app/src/main/kotlin/com/meloda/fast/io/EasyStreams.kt deleted file mode 100644 index af371aa5..00000000 --- a/app/src/main/kotlin/com/meloda/fast/io/EasyStreams.kt +++ /dev/null @@ -1,174 +0,0 @@ -package com.meloda.fast.io - -import org.jetbrains.annotations.Contract -import java.io.* -import java.nio.charset.Charset -import java.util.zip.GZIPInputStream -import java.util.zip.GZIPOutputStream -import kotlin.math.max - -object EasyStreams { - - const val BUFFER_SIZE = 8192 - const val CHAR_BUFFER_SIZE = 4096 - - @JvmOverloads - @Throws(IOException::class) - fun read(from: InputStream, encoding: Charset? = Charsets.UTF_8): String { - return read(InputStreamReader(from, encoding)) - } - - @JvmStatic - @Throws(IOException::class) - fun read(from: Reader): String { - val builder = StringWriter(CHAR_BUFFER_SIZE) - return try { - copy(from, builder) - builder.toString() - } finally { - close(from) - } - } - - @JvmStatic - @Throws(IOException::class) - fun readBytes(from: InputStream): ByteArray { - val output = ByteArrayOutputStream(max(from.available(), BUFFER_SIZE)) - try { - copy(from, output) - } finally { - close(from) - } - return output.toByteArray() - } - - @Throws(IOException::class) - fun write(from: ByteArray?, to: OutputStream) { - try { - to.write(from) - to.flush() - } finally { - close(to) - } - } - - @Throws(IOException::class) - fun write(from: String?, to: OutputStream?) { - write(from, OutputStreamWriter(to, Charsets.UTF_8)) - } - - @Throws(IOException::class) - fun write(from: CharArray?, to: Writer) { - try { - to.write(from) - to.flush() - } finally { - close(to) - } - } - - @JvmStatic - @Throws(IOException::class) - fun write(from: String?, to: Writer) { - try { - to.write(from) - to.flush() - } finally { - close(to) - } - } - - @Throws(IOException::class) - fun copy(from: Reader, to: Writer): Long { - val buffer = CharArray(CHAR_BUFFER_SIZE) - var read: Int - var total: Long = 0 - while (from.read(buffer).also { read = it } != -1) { - to.write(buffer, 0, read) - total += read.toLong() - } - return total - } - - @Throws(IOException::class) - fun copy(from: InputStream, to: OutputStream): Long { - val buffer = ByteArray(BUFFER_SIZE) - var read: Int - var total: Long = 0 - while (from.read(buffer).also { read = it } != -1) { - to.write(buffer, 0, read) - total += read.toLong() - } - return total - } - - fun buffer(input: InputStream?): BufferedInputStream { - return buffer(input, BUFFER_SIZE) - } - - @Contract("null, _ -> new") - fun buffer(input: InputStream?, size: Int): BufferedInputStream { - return if (input is BufferedInputStream) input else BufferedInputStream(input, size) - } - - fun buffer(output: OutputStream?): BufferedOutputStream { - return buffer(output, BUFFER_SIZE) - } - - @Contract("null, _ -> new") - fun buffer(output: OutputStream?, size: Int): BufferedOutputStream { - return if (output is BufferedOutputStream) output else BufferedOutputStream(output, size) - } - - fun buffer(input: Reader?): BufferedReader { - return buffer(input, CHAR_BUFFER_SIZE) - } - - @Contract("null, _ -> new") - fun buffer(input: Reader?, size: Int): BufferedReader { - return if (input is BufferedReader) input else BufferedReader(input, size) - } - - fun buffer(output: Writer?): BufferedWriter { - return buffer(output, CHAR_BUFFER_SIZE) - } - - @Contract("null, _ -> new") - fun buffer(output: Writer?, size: Int): BufferedWriter { - return if (output is BufferedWriter) output else BufferedWriter(output, size) - } - - @Throws(IOException::class) - fun gzip(input: InputStream?): GZIPInputStream { - return gzip(input, BUFFER_SIZE) - } - - @Contract("null, _ -> new") - @Throws(IOException::class) - fun gzip(input: InputStream?, size: Int): GZIPInputStream { - return if (input is GZIPInputStream) input else GZIPInputStream(input, size) - } - - @Throws(IOException::class) - fun gzip(input: OutputStream?): GZIPOutputStream { - return gzip(input, BUFFER_SIZE) - } - - @Contract("null, _ -> new") - @Throws(IOException::class) - fun gzip(input: OutputStream?, size: Int): GZIPOutputStream { - return if (input is GZIPOutputStream) input else GZIPOutputStream(input, size) - } - - fun close(c: Closeable?): Boolean { - if (c != null) { - try { - c.close() - return true - } catch (e: IOException) { - e.printStackTrace() - } - } - return false - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/io/FileStreams.kt b/app/src/main/kotlin/com/meloda/fast/io/FileStreams.kt deleted file mode 100644 index f02f7e7d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/io/FileStreams.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.meloda.fast.io - -import org.jetbrains.annotations.Contract -import java.io.* -import java.math.BigInteger - -object FileStreams { - - val lineSeparatorChar = lineSeparator()[0] - - const val ONE_KB = 1024 - const val ONE_MB = ONE_KB * 1024 - const val ONE_GB = ONE_MB * 1024 - const val ONE_TB = ONE_GB * 1024L - const val ONE_PB = ONE_TB * 1024L - const val ONE_EB = ONE_PB * 1024L - - val ONE_ZB: BigInteger = BigInteger.valueOf(ONE_EB).multiply(BigInteger.valueOf(1024L)) - val ONE_YB: BigInteger = ONE_ZB.multiply(BigInteger.valueOf(1024L)) - - @Throws(IOException::class) - fun read(from: File?): String { - return EasyStreams.read(reader(from)) - } - - @Throws(IOException::class) - fun write(from: String?, to: File?) { - EasyStreams.write(from, writer(to)) - } - - @Throws(IOException::class) - fun write(from: ByteArray?, to: File?) { - EasyStreams.write(from, FileOutputStream(to)) - } - - @Throws(IOException::class) - fun append(from: ByteArray?, to: File?) { - EasyStreams.write(from, FileOutputStream(to, true)) - } - - @Throws(IOException::class) - fun append(from: CharArray?, to: File?) { - EasyStreams.write(from, FileWriter(to, true)) - } - - @Throws(IOException::class) - fun append(from: CharSequence, to: File?) { - EasyStreams.write(if (from is String) from else from.toString(), FileWriter(to, true)) - } - - fun delete(dir: File) { - if (dir.isDirectory) { - val files = dir.listFiles() ?: return - for (file in files) { - delete(file) - } - } else { - dir.delete() - } - } - - fun lineSeparator(): String { - return System.lineSeparator() - } - - fun search(dir: File, name: String?): File? { - require(dir.isDirectory) { "dir can't be file." } - - val files = dir.listFiles() ?: return null - - if (files.isEmpty()) { - return null - } - - for (file in files) { - if (file.isDirectory) { - search(file, name) - } else if (file.name.contains(name!!)) { - return file - } - } - return null - } - - @Contract("_ -> new") - @Throws(FileNotFoundException::class) - fun reader(from: File?): Reader { - return InputStreamReader(FileInputStream(from), Charsets.UTF_8) - } - - @Contract("_ -> new") - @Throws(FileNotFoundException::class) - fun writer(to: File?): Writer { - return OutputStreamWriter(FileOutputStream(to), Charsets.UTF_8) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt index 358beb6d..a0024f3f 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 @@ -6,7 +6,6 @@ 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 @@ -42,10 +41,6 @@ import kotlin.math.roundToInt class ConversationsFragment : BaseViewModelFragment(R.layout.fragment_conversations) { - companion object { - const val TAG = "ConversationsFragment" - } - override val viewModel: ConversationsViewModel by viewModels() private val binding: FragmentConversationsBinding by viewBinding() @@ -80,7 +75,6 @@ class ConversationsFragment : } private var isPaused = false - private var isExpanded = true override fun onPause() { super.onPause() @@ -109,46 +103,24 @@ class ConversationsFragment : binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset -> if (isPaused) return@OnOffsetChangedListener - -// if (verticalOffset <= -100) { -// binding.avatarContainer.alpha = 0f -// return@OnOffsetChangedListener -// } - - // 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() + val padding = AndroidUtils.px(if (verticalOffset <= -100) 10 else 30).roundToInt() binding.avatarContainer.updatePadding( - bottom = value, - right = endPadding + bottom = padding, + right = padding ) + val minusAlpha = (1 - (abs(verticalOffset) * 0.01)).toFloat() + val plusAlpha = (abs(1 + verticalOffset * 0.01) * 1.01).toFloat() - println("Fast::ConversationsFragment::onOffset verticalOffset = $verticalOffset; bottomPadding = $value; endPadding = $endPadding") + println("Fast::ConversationsFragment::onOffset minusAlpha: $minusAlpha; plusAlpha: $plusAlpha") + val alpha: Float = if (verticalOffset <= -100) plusAlpha else minusAlpha -// binding.avatarContainer.alpha = alpha + binding.avatarContainer.alpha = alpha }) - binding.toolbar.overflowIcon = ContextCompat.getDrawable(requireContext(), R.drawable.test) - - - binding.avatar.setOnClickListener { - avatarPopupMenu.show() - } + binding.avatar.setOnClickListener { avatarPopupMenu.show() } binding.avatar.setOnLongClickListener { lifecycleScope.launch { 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 f3bd08db..866d7f80 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 @@ -6,11 +6,11 @@ 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.VkUser -import com.meloda.fast.api.model.request.ConversationsDeleteRequest -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.api.network.conversations.ConversationsDataSource +import com.meloda.fast.api.network.conversations.ConversationsDeleteRequest +import com.meloda.fast.api.network.conversations.ConversationsGetRequest +import com.meloda.fast.api.network.users.UsersDataSource +import com.meloda.fast.api.network.users.UsersGetRequest import com.meloda.fast.base.viewmodel.BaseViewModel import com.meloda.fast.base.viewmodel.VkEvent import dagger.hilt.android.lifecycle.HiltViewModel @@ -34,7 +34,7 @@ class ConversationsViewModel @Inject constructor( count = 30, extended = true, offset = offset, - fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}" + fields = "${VKConstants.ALL_FIELDS}" ) ) }, diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt index 330344e9..f514ba12 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt @@ -4,8 +4,8 @@ import androidx.lifecycle.viewModelScope import com.meloda.fast.api.UserConfig import com.meloda.fast.api.VKConstants import com.meloda.fast.api.VKException -import com.meloda.fast.api.model.request.RequestAuthDirect -import com.meloda.fast.api.network.datasource.AuthDataSource +import com.meloda.fast.api.network.auth.RequestAuthDirect +import com.meloda.fast.api.network.auth.AuthDataSource import com.meloda.fast.base.viewmodel.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch 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 2295ffcd..6f76f407 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt @@ -6,8 +6,7 @@ import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkUser -import com.meloda.fast.api.model.request.* -import com.meloda.fast.api.network.datasource.MessagesDataSource +import com.meloda.fast.api.network.messages.* import com.meloda.fast.base.viewmodel.BaseViewModel import com.meloda.fast.base.viewmodel.VkEvent import dagger.hilt.android.lifecycle.HiltViewModel @@ -28,7 +27,7 @@ class MessagesHistoryViewModel @Inject constructor( count = 30, peerId = peerId, extended = true, - fields = "${VKConstants.USER_FIELDS},${VKConstants.GROUP_FIELDS}" + fields = VKConstants.ALL_FIELDS ) ) }, diff --git a/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt b/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt index 6a99ef8c..45d9e807 100644 --- a/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt +++ b/app/src/main/kotlin/com/meloda/fast/service/LongPollService.kt @@ -1,9 +1,9 @@ package com.meloda.fast.service import android.util.Log -import com.meloda.fast.api.model.request.MessagesGetLongPollServerRequest -import com.meloda.fast.api.network.datasource.MessagesDataSource -import com.meloda.fast.api.network.repo.LongPollRepo +import com.meloda.fast.api.network.messages.MessagesGetLongPollServerRequest +import com.meloda.fast.api.network.messages.MessagesDataSource +import com.meloda.fast.api.network.longpoll.LongPollRepo import kotlinx.coroutines.* import javax.inject.Inject import kotlin.coroutines.CoroutineContext diff --git a/app/src/main/kotlin/com/meloda/fast/util/ArrayUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/ArrayUtils.kt deleted file mode 100644 index de96a79d..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/ArrayUtils.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.meloda.fast.util - -import java.util.stream.Collectors - -object ArrayUtils { - - @SafeVarargs - fun asString(vararg array: T): String { - if (array.isEmpty()) { - return "" - } - - val builder = StringBuilder(array.size * 12) - builder.append(array[0]) - for (i in 1 until array.size) { - builder.append(',') - builder.append(array[i]) - } - return builder.toString() - } - - fun asString(array: IntArray): String { - if (array.isEmpty()) { - return "" - } - - val builder = StringBuilder(array.size * 12) - builder.append(array[0]) - for (i in 1 until array.size) { - builder.append(',') - builder.append(array[i]) - } - return builder.toString() - } - - fun asString(arrayList: ArrayList): String { - return ArrayList().apply { - arrayList.forEach { add(it.toString()) } - }.stream().collect(Collectors.joining(",")) - } - - fun asString(list: List): String = asString(list.asArrayList()) - - fun cut(arrayList: ArrayList, offset: Int, count: Int): ArrayList { - if (arrayList.isEmpty()) return arrayListOf() - - var lastPosition = offset + count - if (lastPosition > arrayList.size) lastPosition = arrayList.size - - return ArrayList(arrayList.subList(offset, lastPosition)) - } - - fun ByteArray?.isNullOrEmpty() = this == null || this.isEmpty() - - fun List.asArrayList(): ArrayList { - return ArrayList(this) - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt deleted file mode 100644 index 9e038147..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/ColorUtils.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.meloda.fast.util - -import android.content.Context -import android.graphics.Color -import androidx.annotation.ColorInt -import com.meloda.fast.R - -object ColorUtils { - - @ColorInt - fun getColorAccent(context: Context): Int { - return AndroidUtils.getThemeAttrColor(context, R.attr.colorAccent) - } - - @ColorInt - fun getColorPrimary(context: Context): Int { - return AndroidUtils.getThemeAttrColor(context, R.attr.colorPrimary) - } - - @JvmOverloads - fun darkenColor(color: Int, darkFactor: Float = 0.75f): Int { - var newColor = color - val hsv = FloatArray(3) - Color.colorToHSV(newColor, hsv) - hsv[2] *= darkFactor - newColor = Color.HSVToColor(hsv) - return newColor - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/util/ImageUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/ImageUtils.kt deleted file mode 100644 index c1d2c056..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/ImageUtils.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.meloda.fast.util - -import android.graphics.Bitmap -import android.graphics.drawable.Drawable -import android.widget.ImageView - -object ImageUtils { - - fun loadImage(image: String, imageView: ImageView, placeholder: Drawable?) { - if (image.isEmpty()) return - -// if (imageView is SimpleDraweeView) { -// imageView.setImageURI(image) -// return -// } -// -// val picasso = Picasso.get() -// .load(image) -// .priority(Picasso.Priority.LOW) - -// if (placeholder != null) picasso.placeholder(placeholder) -// -// picasso.into(imageView) - } - - fun loadImage(image: String?, listener: OnLoadListener?) { - if (image.isNullOrEmpty()) return - -// val picasso = Picasso.get() -// .load(image) -// .priority(Picasso.Priority.LOW) -// -// val target = object : Target { -// override fun onPrepareLoad(placeHolderDrawable: Drawable?) { -// -// } -// -// override fun onBitmapFailed(e: Exception, errorDrawable: Drawable?) { -// listener?.onError(e) -// } -// -// override fun onBitmapLoaded(bitmap: Bitmap, from: Picasso.LoadedFrom) { -// listener?.onLoad(bitmap) -// } -// } - -// picasso.into(target) - } - - - interface OnLoadListener { - fun onLoad(bitmap: Bitmap) - fun onError(e: Exception) - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/util/TextUtils.kt b/app/src/main/kotlin/com/meloda/fast/util/TextUtils.kt deleted file mode 100644 index d8524e37..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/TextUtils.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.meloda.fast.util - -object TextUtils { - - fun getFirstLetterFromString(string: String): String { - for (i in string.indices) { - val char = string[i] - - if (char.isLetter()) return char.toString() - } - - return "" - } - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/util/Utils.kt b/app/src/main/kotlin/com/meloda/fast/util/Utils.kt deleted file mode 100644 index 32497083..00000000 --- a/app/src/main/kotlin/com/meloda/fast/util/Utils.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.meloda.fast.util - -import android.content.Context -import com.meloda.fast.util.ArrayUtils.isNullOrEmpty -import com.meloda.fast.R -import com.meloda.fast.io.BytesOutputStream -import java.io.ByteArrayInputStream -import java.io.IOException -import java.io.ObjectInputStream -import java.io.ObjectOutputStream - -object Utils { - - fun getLocalizedThrowable(context: Context, t: Throwable): String { - return context.getString(R.string.error, t.message.toString()) - } - - fun serialize(source: Any?): ByteArray? { - try { - val bos = BytesOutputStream() - val out = ObjectOutputStream(bos) - out.writeObject(source) - out.close() - return bos.byteArray - } catch (e: IOException) { - e.printStackTrace() - } - return null - } - - fun deserialize(source: ByteArray?): Any? { - if (source.isNullOrEmpty()) { - return null - } - - try { - val bis = ByteArrayInputStream(source) - val `in` = ObjectInputStream(bis) - val o = `in`.readObject() - `in`.close() - return o - } catch (e: Exception) { - e.printStackTrace() - } - return null - } - - -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/widget/NoItemsView.kt b/app/src/main/kotlin/com/meloda/fast/widget/NoItemsView.kt index 5c1c9729..f6770643 100644 --- a/app/src/main/kotlin/com/meloda/fast/widget/NoItemsView.kt +++ b/app/src/main/kotlin/com/meloda/fast/widget/NoItemsView.kt @@ -10,12 +10,11 @@ import android.widget.TextView import androidx.annotation.ColorInt import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import androidx.appcompat.content.res.AppCompatResources import androidx.core.view.isVisible -import com.meloda.fast.extensions.ContextExtensions.drawable -import com.meloda.fast.extensions.DrawableExtensions.tint -import com.meloda.fast.extensions.FloatExtensions.int import com.meloda.fast.R import com.meloda.fast.util.AndroidUtils +import kotlin.math.roundToInt @Suppress("UNCHECKED_CAST") class NoItemsView @JvmOverloads constructor( @@ -44,7 +43,7 @@ class NoItemsView @JvmOverloads constructor( private fun create() { val a = context.obtainStyledAttributes(attrs, R.styleable.NoItemsView) - minimumWidth = AndroidUtils.px(256).int() + minimumWidth = AndroidUtils.px(256).roundToInt() minimumHeight = minimumWidth orientation = VERTICAL @@ -53,8 +52,8 @@ class NoItemsView @JvmOverloads constructor( noItemsPicture = ImageView(context) val params = imageViewParams - params.height = AndroidUtils.px(64).int() - params.width = AndroidUtils.px(64).int() + params.height = AndroidUtils.px(64).roundToInt() + params.width = AndroidUtils.px(64).roundToInt() noItemsPicture.layoutParams = params @@ -73,10 +72,10 @@ class NoItemsView @JvmOverloads constructor( noItemsTextView = TextView(context) val textParams = textViewParams - textParams.width = AndroidUtils.px(256).int() + textParams.width = AndroidUtils.px(256).roundToInt() if (noItemsDrawable != null) { - textParams.topMargin = AndroidUtils.px(8).int() + textParams.topMargin = AndroidUtils.px(8).roundToInt() } noItemsTextView.layoutParams = textParams @@ -103,7 +102,7 @@ class NoItemsView @JvmOverloads constructor( } fun setNoItemsImage(@DrawableRes resId: Int) { - setNoItemsImage(context.drawable(resId)) + setNoItemsImage(AppCompatResources.getDrawable(context, resId)) } fun setNoItemsImage(drawable: Drawable?) { @@ -111,7 +110,7 @@ class NoItemsView @JvmOverloads constructor( } fun setNoItemsImageTint(@ColorInt color: Int) { - noItemsPicture.drawable.tint(color) + noItemsPicture.drawable?.setTint(color) } fun setNoItemsText(@StringRes resId: Int) { diff --git a/app/src/main/res/drawable/test.png b/app/src/main/res/drawable/test.png deleted file mode 100644 index 6da07119a62140c0a6db945881691edd078ee472..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44277 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 t(AKHz=yu4)c4ET + android:paddingBottom="30dp" + app:layout_collapseMode="none"> - + + + + + + + + + - + + + + + + + + - + + + + + - + diff --git a/app/src/main/res/menu/fragment_conversations.xml b/app/src/main/res/menu/fragment_conversations.xml index aab207c3..e006374c 100644 --- a/app/src/main/res/menu/fragment_conversations.xml +++ b/app/src/main/res/menu/fragment_conversations.xml @@ -2,5 +2,10 @@ + + + + + \ No newline at end of file From 118c531d003305a38ba68033363a881251022561 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sun, 10 Oct 2021 18:38:43 +0300 Subject: [PATCH 07/15] fix VkLink.kt target NullPointerException --- .../com/meloda/fast/api/ApiExtensions.kt | 1 - .../fast/api/model/attachments/VkLink.kt | 2 +- .../api/model/base/attachments/BaseVkLink.kt | 2 +- .../com/meloda/fast/base/BaseActivity.kt | 8 -------- .../com/meloda/fast/common/AppSettings.kt | 1 + .../conversations/ConversationsFragment.kt | 2 +- .../fast/screens/login/LoginFragment.kt | 9 ++++++--- .../fast/screens/login/LoginViewModel.kt | 19 ++++++++++++------- .../meloda/fast/screens/main/MainFragment.kt | 1 - 9 files changed, 22 insertions(+), 23 deletions(-) diff --git a/app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt b/app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt index 95b757b1..83a8ee8c 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/ApiExtensions.kt @@ -4,5 +4,4 @@ object ApiExtensions { val Boolean.intString get() = (if (this) 1 else 0).toString() - } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt index 2a4a9cd0..321c71ac 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkLink.kt @@ -9,7 +9,7 @@ data class VkLink( val title: String?, val caption: String?, val photo: VkPhoto?, - val target: String, + val target: String?, val isFavorite: Boolean ) : VkAttachment() { diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt index c127f0c2..4f1b59c5 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkLink.kt @@ -9,7 +9,7 @@ data class BaseVkLink( val title: String?, val caption: String?, val photo: BaseVkPhoto?, - val target: String, + val target: String?, val is_favorite: Boolean ) : BaseVkAttachment() { diff --git a/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt b/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt index 1c910dad..a3e6804e 100644 --- a/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt +++ b/app/src/main/kotlin/com/meloda/fast/base/BaseActivity.kt @@ -1,13 +1,11 @@ package com.meloda.fast.base import android.os.Bundle -import android.view.View import androidx.annotation.LayoutRes import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleRegistry -import com.google.android.material.snackbar.Snackbar abstract class BaseActivity : AppCompatActivity, LifecycleOwner { @@ -39,10 +37,4 @@ abstract class BaseActivity : AppCompatActivity, LifecycleOwner { lifecycleRegistry.currentState = Lifecycle.State.DESTROYED } - val rootView: View? get() = findViewById(android.R.id.content) - - fun requireRootView() = rootView!! - - var errorSnackbar: Snackbar? = null - } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/common/AppSettings.kt b/app/src/main/kotlin/com/meloda/fast/common/AppSettings.kt index 1f9d8b6b..c4039eb1 100644 --- a/app/src/main/kotlin/com/meloda/fast/common/AppSettings.kt +++ b/app/src/main/kotlin/com/meloda/fast/common/AppSettings.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.Job object AppSettings { val keyIsMultilineEnabled = booleanPreferencesKey("isMultilineEnabled") + } val Context.dataStore: DataStore by preferencesDataStore( diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt index a0024f3f..dc52b546 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 @@ -91,7 +91,7 @@ class ConversationsFragment : requireContext().dataStore.data.map { adapter.isMultilineEnabled = it[AppSettings.keyIsMultilineEnabled] ?: true adapter.notifyItemRangeChanged(0, adapter.itemCount) - }.collect { } + }.collect() } binding.createChat.setOnClickListener {} 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 ce4b2417..d808d711 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginFragment.kt @@ -77,7 +77,7 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo is ErrorEvent -> showErrorSnackbar(event.errorText) is CaptchaEvent -> showCaptchaDialog(event.sid, event.image) is ValidationEvent -> showValidationRequired(event.sid) - is SuccessAuth -> goToMain(event.haveAuthorized) + is SuccessAuth -> goToMain(event) is CodeSent -> showValidationDialog() is StartProgressEvent -> onProgressStarted() @@ -384,8 +384,11 @@ class LoginFragment : BaseViewModelFragment(R.layout.fragment_lo snackbar.show() } - private fun goToMain(haveAuthorized: Boolean) = lifecycleScope.launch { - if (haveAuthorized) delay(500) + private fun goToMain(event: SuccessAuth) = lifecycleScope.launch { + UserConfig.userId = event.userId + UserConfig.accessToken = event.vkToken + + if (event.haveAuthorized) delay(500) launchWebView() diff --git a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt index f514ba12..c4666d32 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/login/LoginViewModel.kt @@ -1,11 +1,10 @@ package com.meloda.fast.screens.login import androidx.lifecycle.viewModelScope -import com.meloda.fast.api.UserConfig import com.meloda.fast.api.VKConstants import com.meloda.fast.api.VKException -import com.meloda.fast.api.network.auth.RequestAuthDirect import com.meloda.fast.api.network.auth.AuthDataSource +import com.meloda.fast.api.network.auth.RequestAuthDirect import com.meloda.fast.base.viewmodel.* import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch @@ -45,10 +44,12 @@ class LoginViewModel @Inject constructor( return@makeJob } - UserConfig.userId = it.userId - UserConfig.accessToken = it.accessToken - - sendEvent(SuccessAuth()) + sendEvent( + SuccessAuth( + userId = it.userId, + vkToken = it.accessToken + ) + ) }, onError = { if (it !is VKException) { @@ -72,4 +73,8 @@ class LoginViewModel @Inject constructor( object CodeSent : VkEvent() -data class SuccessAuth(val haveAuthorized: Boolean = true) : VkEvent() \ No newline at end of file +data class SuccessAuth( + val haveAuthorized: Boolean = true, + val userId: Int, + val vkToken: String +) : VkEvent() \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt index 58f5fcf9..980c9f8f 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/main/MainFragment.kt @@ -42,5 +42,4 @@ class MainFragment : BaseViewModelFragment(R.layout.fragment_main } } - } \ No newline at end of file From c90b8fb822eaaf3c7cacc03e6c66c5ed7cc5bbc0 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sun, 10 Oct 2021 18:48:22 +0300 Subject: [PATCH 08/15] appBar shadow on scroll --- .../fast/screens/conversations/ConversationsFragment.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 dc52b546..a807c83e 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 @@ -103,6 +103,11 @@ class ConversationsFragment : binding.appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset -> if (isPaused) return@OnOffsetChangedListener + binding.appBar.animate().translationZ( + if (verticalOffset < 0) AndroidUtils.px(3).roundToInt().toFloat() + else 0f + ).setDuration(50).start() + val padding = AndroidUtils.px(if (verticalOffset <= -100) 10 else 30).roundToInt() binding.avatarContainer.updatePadding( @@ -113,7 +118,7 @@ class ConversationsFragment : val minusAlpha = (1 - (abs(verticalOffset) * 0.01)).toFloat() val plusAlpha = (abs(1 + verticalOffset * 0.01) * 1.01).toFloat() - println("Fast::ConversationsFragment::onOffset minusAlpha: $minusAlpha; plusAlpha: $plusAlpha") + println("Fast::ConversationsFragment::onOffset offset: $verticalOffset; minusAlpha: $minusAlpha; plusAlpha: $plusAlpha") val alpha: Float = if (verticalOffset <= -100) plusAlpha else minusAlpha From 147e6c5a33b3c18f3e448217a7dbcd9740bc064f Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sun, 10 Oct 2021 23:04:39 +0300 Subject: [PATCH 09/15] splash screen pin & unpin conversations --- app/build.gradle.kts | 16 ++--- .../com/meloda/fast/activity/MainActivity.kt | 11 +++- .../conversations/ConversationsDataSource.kt | 2 + .../conversations/ConversationsRequest.kt | 10 ++++ .../conversations/ConversationsFragment.kt | 58 +++++++++++++++++-- .../conversations/ConversationsViewModel.kt | 29 ++++++++-- .../res/values/ic_launcher_background.xml | 3 +- app/src/main/res/values/strings.xml | 6 ++ app/src/main/res/values/themes.xml | 5 ++ 9 files changed, 122 insertions(+), 18 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6e8f6777..899d0828 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -82,15 +82,17 @@ dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") + implementation("androidx.core:core-splashscreen:1.0.0-alpha02") + implementation("androidx.work:work-runtime-ktx:2.6.0") implementation("androidx.datastore:datastore-preferences:1.0.0") implementation("androidx.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") + implementation("androidx.appcompat:appcompat:1.4.0-beta01") + implementation("com.google.android.material:material:1.5.0-alpha04") + implementation("androidx.core:core-ktx:1.7.0-beta02") implementation("androidx.preference:preference-ktx:1.1.1") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01") implementation("androidx.recyclerview:recyclerview:1.2.1") @@ -118,16 +120,16 @@ dependencies { implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") - implementation("com.google.dagger:hilt-android:2.38.1") - kapt("com.google.dagger:hilt-android-compiler:2.38.1") + implementation("com.google.dagger:hilt-android:2.39.1") + kapt("com.google.dagger:hilt-android-compiler:2.39.1") implementation("androidx.hilt:hilt-navigation-fragment:1.0.0") implementation("com.github.yogacp:android-viewbinding:1.0.3") - implementation("io.coil-kt:coil:1.3.2") + implementation("io.coil-kt:coil:1.4.0") implementation("com.google.code.gson:gson:2.8.8") - implementation("org.jsoup:jsoup:1.14.2") + implementation("org.jsoup:jsoup:1.14.3") implementation("ch.acra:acra:4.11.1") } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/activity/MainActivity.kt b/app/src/main/kotlin/com/meloda/fast/activity/MainActivity.kt index d7f095d2..e02414e4 100644 --- a/app/src/main/kotlin/com/meloda/fast/activity/MainActivity.kt +++ b/app/src/main/kotlin/com/meloda/fast/activity/MainActivity.kt @@ -1,8 +1,17 @@ package com.meloda.fast.activity +import android.os.Bundle +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import com.meloda.fast.R import com.meloda.fast.base.BaseActivity import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint -class MainActivity : BaseActivity(R.layout.activity_main) \ No newline at end of file +class MainActivity : BaseActivity(R.layout.activity_main) { + + override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + super.onCreate(savedInstanceState) + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsDataSource.kt index 515dd4b5..838a5a8f 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsDataSource.kt @@ -13,7 +13,9 @@ class ConversationsDataSource @Inject constructor( suspend fun delete(params: ConversationsDeleteRequest) = repo.delete(params.map) + suspend fun pin(params: ConversationsPinRequest) = repo.pin(params.map) + suspend fun unpin(params: ConversationsUnpinRequest) = repo.unpin(params.map) suspend fun store(conversations: List) = dao.insert(conversations) diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRequest.kt index 81fdca5d..9bd9a622 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/conversations/ConversationsRequest.kt @@ -28,4 +28,14 @@ data class ConversationsGetRequest( @Parcelize data class ConversationsDeleteRequest(val peerId: Int) : Parcelable { val map get() = mapOf("peer_id" to peerId.toString()) +} + +@Parcelize +data class ConversationsPinRequest(val peerId: Int) : Parcelable { + val map get() = mapOf("peer_id" to peerId.toString()) +} + +@Parcelize +data class ConversationsUnpinRequest(val peerId: Int) : Parcelable { + val map get() = mapOf("peer_id" to peerId.toString()) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt index a807c83e..e63621a3 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsFragment.kt @@ -187,6 +187,9 @@ class ConversationsFragment : is ConversationsLoaded -> refreshConversations(event) is ConversationsDelete -> deleteConversation(event.peerId) + + // TODO: 10-Oct-21 remove this and sort conversations list + is ConversationsPin, is ConversationsUnpin -> viewModel.loadConversations() } } @@ -240,13 +243,19 @@ class ConversationsFragment : private fun fillRecyclerView(values: List) { adapter.values.clear() adapter.values += values - adapter.notifyItemRangeChanged(0, adapter.itemCount) + adapter.submitList(values) } private fun onItemClick(position: Int) { val conversation = adapter[position] - val user = if (conversation.isUser()) adapter.profiles[conversation.id] else null - val group = if (conversation.isGroup()) adapter.groups[conversation.id] else null + + val user = + if (conversation.isUser()) adapter.profiles[conversation.id] + else null + + val group = + if (conversation.isGroup()) adapter.groups[conversation.id] + else null findNavController().navigate( R.id.toMessagesHistory, @@ -266,15 +275,36 @@ class ConversationsFragment : private fun showOptionsDialog(position: Int) { val conversation = adapter[position] + var canPinOneMoreDialog = true + if (adapter.itemCount > 4) { + val firstFiveDialogs = adapter.values.subList(0, 5) + var pinnedCount = 0 + + firstFiveDialogs.forEach { if (it.isPinned) pinnedCount++ } + if (pinnedCount == 5 && position > 4) { + canPinOneMoreDialog = false + } + } + + val pin = getString( + if (conversation.isPinned) R.string.conversation_context_action_unpin + else R.string.conversation_context_action_pin + ) + val delete = getString(R.string.conversation_context_action_delete) - val params = mutableListOf(delete) + val params = mutableListOf() + + if (canPinOneMoreDialog) params += pin + + params += delete val arrayParams = params.toTypedArray() MaterialAlertDialogBuilder(requireContext()) .setItems(arrayParams) { _, which -> when (params[which]) { + pin -> showPinConversationDialog(conversation) delete -> showDeleteConversationDialog(conversation.id) } }.show() @@ -295,4 +325,24 @@ class ConversationsFragment : adapter.notifyItemRemoved(index) } + private fun showPinConversationDialog(conversation: VkConversation) { + val isPinned = conversation.isPinned + MaterialAlertDialogBuilder(requireContext()) + .setTitle( + if (isPinned) R.string.confirm_unpin_conversation + else R.string.confirm_pin_conversation + ) + .setPositiveButton( + if (isPinned) R.string.action_unpin + else R.string.action_pin + ) { _, _ -> + viewModel.pinConversation( + peerId = conversation.id, + pin = !isPinned + ) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt index 866d7f80..c750edd0 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/conversations/ConversationsViewModel.kt @@ -6,9 +6,7 @@ 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.VkUser -import com.meloda.fast.api.network.conversations.ConversationsDataSource -import com.meloda.fast.api.network.conversations.ConversationsDeleteRequest -import com.meloda.fast.api.network.conversations.ConversationsGetRequest +import com.meloda.fast.api.network.conversations.* import com.meloda.fast.api.network.users.UsersDataSource import com.meloda.fast.api.network.users.UsersGetRequest import com.meloda.fast.base.viewmodel.BaseViewModel @@ -34,7 +32,7 @@ class ConversationsViewModel @Inject constructor( count = 30, extended = true, offset = offset, - fields = "${VKConstants.ALL_FIELDS}" + fields = VKConstants.ALL_FIELDS ) ) }, @@ -88,6 +86,23 @@ class ConversationsViewModel @Inject constructor( ) }, onAnswer = { sendEvent(ConversationsDelete(peerId)) }) } + + fun pinConversation( + peerId: Int, + pin: Boolean + ) = viewModelScope.launch { + if (pin) { + makeJob( + { conversations.pin(ConversationsPinRequest(peerId)) }, + onAnswer = { sendEvent(ConversationsPin(peerId)) } + ) + } else { + makeJob( + { conversations.unpin(ConversationsUnpinRequest(peerId)) }, + onAnswer = { sendEvent(ConversationsUnpin(peerId)) } + ) + } + } } data class ConversationsLoaded( @@ -99,4 +114,8 @@ data class ConversationsLoaded( val groups: HashMap ) : VkEvent() -data class ConversationsDelete(val peerId: Int) : VkEvent() \ No newline at end of file +data class ConversationsDelete(val peerId: Int) : VkEvent() + +data class ConversationsPin(val peerId: Int) : VkEvent() + +data class ConversationsUnpin(val peerId: Int) : VkEvent() \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml index f1503161..0deb0bf0 100644 --- a/app/src/main/res/values/ic_launcher_background.xml +++ b/app/src/main/res/values/ic_launcher_background.xml @@ -1,4 +1,5 @@ - #4184F5 + + @color/a1_500 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1fabaf95..989b5d0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -134,4 +134,10 @@ Delete the conversation? Sign out Sign out + Unpin + Pin + Unpin the conversation? + Pin the conversation? + Pin + Unpin diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 145b8b2f..517ae724 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -41,5 +41,10 @@ 12dp + \ No newline at end of file From ef7d1a603106da6b890ab14285ccd268bcfd8166 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Mon, 11 Oct 2021 00:06:36 +0300 Subject: [PATCH 10/15] call in attachments --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 1 + .../kotlin/com/meloda/fast/api/VkUtils.kt | 8 +-- .../fast/api/model/attachments/VkCall.kt | 7 ++- .../api/model/attachments/VkVoiceMessage.kt | 11 +++- .../api/model/base/attachments/BaseVkCall.kt | 14 ++++- .../base/attachments/BaseVkVoiceMessage.kt | 17 +++++- .../screens/messages/AttachmentInflater.kt | 58 +++++++++++++++++- .../messages/MessagesHistoryAdapter.kt | 4 ++ .../screens/messages/MessagesPreparator.kt | 4 +- .../layout/item_message_attachment_call.xml | 59 +++++++++++++++++++ .../layout/item_message_attachment_voice.xml | 56 ++++++++++++++++++ app/src/main/res/layout/item_message_in.xml | 2 +- app/src/main/res/layout/item_message_out.xml | 19 ++++-- app/src/main/res/values-v31/colors.xml | 1 + app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 6 ++ 17 files changed, 251 insertions(+), 21 deletions(-) create mode 100644 app/src/main/res/layout/item_message_attachment_call.xml create mode 100644 app/src/main/res/layout/item_message_attachment_voice.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 899d0828..0117e214 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -82,6 +82,8 @@ dependencies { coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") + implementation("com.github.massoudss:waveformSeekBar:3.1.0") + implementation("androidx.core:core-splashscreen:1.0.0-alpha02") implementation("androidx.work:work-runtime-ktx:2.6.0") @@ -100,7 +102,7 @@ dependencies { implementation("androidx.fragment:fragment-ktx:1.3.6") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2-native-mt") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2") implementation("androidx.room:room-ktx:2.3.0") implementation("androidx.room:room-runtime:2.3.0") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1486ffe8..4e07d606 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,6 +9,7 @@ { val voiceMessage = baseAttachment.voiceMessage ?: continue - attachments += VkVoiceMessage( - link = voiceMessage.link_mp3 - ) + attachments += voiceMessage.asVkVoiceMessage() } BaseVkAttachmentItem.AttachmentType.STICKER -> { val sticker = baseAttachment.sticker ?: continue @@ -180,9 +178,7 @@ object VkUtils { } BaseVkAttachmentItem.AttachmentType.CALL -> { val call = baseAttachment.call ?: continue - attachments += VkCall( - initiatorId = call.initiator_id - ) + attachments += call.asVkCall() } BaseVkAttachmentItem.AttachmentType.GROUP_CALL_IN_PROGRESS -> { val groupCall = baseAttachment.groupCall ?: continue diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt index 43c54a00..c55143da 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkCall.kt @@ -5,7 +5,12 @@ import kotlinx.parcelize.Parcelize @Parcelize data class VkCall( - val initiatorId: Int + val initiatorId: Int, + val receiverId: Int, + val state: String, + val time: Int, + val duration: Int, + val isVideo: Boolean ) : VkAttachment() { @IgnoredOnParcel diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt index 5ead3805..6fcce196 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVoiceMessage.kt @@ -5,9 +5,18 @@ import kotlinx.parcelize.Parcelize @Parcelize data class VkVoiceMessage( - val link: String + val id: Int, + val ownerId: Int, + val duration: Int, + val waveform: List, + val linkOgg: String, + val linkMp3: String, + val accessKey: String, + val transcriptState: String, + val transcript: String ) : VkAttachment() { @IgnoredOnParcel val className: String = this::class.java.name + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt index 9911e79a..2bbde082 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkCall.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.model.base.attachments import android.os.Parcelable +import com.meloda.fast.api.model.attachments.VkCall import kotlinx.parcelize.Parcelize @Parcelize @@ -11,4 +12,15 @@ data class BaseVkCall( val time: Int, val duration: Int, val video: Boolean -) : Parcelable \ No newline at end of file +) : Parcelable { + + fun asVkCall() = VkCall( + initiatorId = initiator_id, + receiverId = receiver_id, + state = state, + time = time, + duration = duration, + isVideo = video + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt index 88b37355..4445ffe0 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVoiceMessage.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.model.base.attachments import android.os.Parcelable +import com.meloda.fast.api.model.attachments.VkVoiceMessage import kotlinx.parcelize.Parcelize @Parcelize @@ -14,4 +15,18 @@ data class BaseVkVoiceMessage( val access_key: String, val transcript_state: String, val transcript: String -) : Parcelable \ No newline at end of file +) : Parcelable { + + fun asVkVoiceMessage() = VkVoiceMessage( + id = id, + ownerId = owner_id, + duration = duration, + waveform = waveform, + linkOgg = link_ogg, + linkMp3 = link_mp3, + accessKey = access_key, + transcriptState = transcript_state, + transcript = transcript + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt index 5e231f39..902748af 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt @@ -15,9 +15,11 @@ import androidx.core.content.ContextCompat import androidx.core.view.isNotEmpty import androidx.core.view.isVisible import androidx.core.view.setPadding +import androidx.core.view.updatePadding import coil.load import com.google.android.material.imageview.ShapeableImageView import com.meloda.fast.R +import com.meloda.fast.api.UserConfig import com.meloda.fast.api.VkUtils import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkMessage @@ -34,6 +36,7 @@ import kotlin.math.roundToInt class AttachmentInflater constructor( private val context: Context, private val container: LinearLayoutCompat, + private val textContainer: LinearLayoutCompat, private val message: VkMessage, private val profiles: Map, private val groups: Map @@ -57,6 +60,7 @@ class AttachmentInflater constructor( attachments = message.attachments!! container.removeAllViews() + textContainer.removeAllViews() if (attachments.size == 1) { when (val attachment = attachments[0]) { @@ -90,7 +94,8 @@ class AttachmentInflater constructor( is VkAudio -> audio(attachment) is VkFile -> file(attachment) is VkLink -> link(attachment) - is VkStory -> story(attachment) + is VkVoiceMessage -> voice(attachment) + is VkCall -> call(attachment) else -> Log.e( "Attachment inflater", @@ -312,8 +317,57 @@ class AttachmentInflater constructor( ).format(wall.date * 1000L) } - private fun story(story: VkStory) { + private fun voice(voiceMessage: VkVoiceMessage) { + val binding = ItemMessageAttachmentVoiceBinding.inflate(inflater, textContainer, true) + if (message.isOut) + binding.root.updatePadding( + bottom = AndroidUtils.px(5).roundToInt(), + left = AndroidUtils.px(6).roundToInt() + ) + + val waveform = IntArray(voiceMessage.waveform.size) + voiceMessage.waveform.forEachIndexed { index, i -> waveform[index] = i } + + binding.waveform.sample = waveform + binding.waveform.maxProgress = 100f + binding.waveform.progress = 100f + + binding.duration.text = SimpleDateFormat( + "mm:ss", + Locale.getDefault() + ).format(voiceMessage.duration * 1000L) + } + + private fun call(call: VkCall) { + val binding = ItemMessageAttachmentCallBinding.inflate(inflater, textContainer, true) + + if (message.isOut) + binding.root.updatePadding( + bottom = AndroidUtils.px(5).roundToInt(), + left = AndroidUtils.px(6).roundToInt() + ) + + val callType = + context.getString( + if (call.initiatorId == UserConfig.userId) R.string.message_call_type_outgoing + else R.string.message_call_type_incoming + ) + + binding.type.text = callType + + var callState = + context.getString( + if (call.state == "reached") R.string.message_call_state_ended + else if (call.state == "canceled_by_initiator") { + if (call.initiatorId == UserConfig.userId) R.string.message_call_state_cancelled + else R.string.message_call_state_missed + } else R.string.message_call_unknown + ) + + if (callState == context.getString(R.string.message_call_unknown)) callState = call.state + + binding.state.text = callState } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt index 29c0b9ce..426b8a24 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 @@ -136,6 +136,8 @@ class MessagesHistoryAdapter constructor( text = binding.text, spacer = binding.spacer, unread = binding.unread, + + textContainer = binding.textContainer, attachmentContainer = binding.attachmentContainer, attachmentSpacer = binding.attachmentSpacer, @@ -172,6 +174,8 @@ class MessagesHistoryAdapter constructor( text = binding.text, spacer = binding.spacer, unread = binding.unread, + + textContainer = binding.textContainer, attachmentContainer = binding.attachmentContainer, attachmentSpacer = binding.attachmentSpacer, 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 c46d9b9e..3cf1de56 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 @@ -42,6 +42,7 @@ class MessagesPreparator constructor( private val spacer: Space? = null, private val unread: ImageView? = null, private val time: TextView? = null, + private val textContainer: LinearLayoutCompat? = null, private val attachmentContainer: LinearLayoutCompat? = null, private val attachmentSpacer: Space? = null, @@ -158,7 +159,7 @@ class MessagesPreparator constructor( } private fun prepareAttachments() { - if (attachmentContainer != null) { + if (attachmentContainer != null && textContainer != null) { if (message.attachments.isNullOrEmpty()) { attachmentContainer.isVisible = false attachmentContainer.removeAllViews() @@ -168,6 +169,7 @@ class MessagesPreparator constructor( AttachmentInflater( context = context, container = attachmentContainer, + textContainer = textContainer, message = message, groups = groups, profiles = profiles diff --git a/app/src/main/res/layout/item_message_attachment_call.xml b/app/src/main/res/layout/item_message_attachment_call.xml new file mode 100644 index 00000000..2556aec9 --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_call.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_voice.xml b/app/src/main/res/layout/item_message_attachment_voice.xml new file mode 100644 index 00000000..1a5452bd --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_voice.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_in.xml b/app/src/main/res/layout/item_message_in.xml index a142f1e8..83f5bc99 100644 --- a/app/src/main/res/layout/item_message_in.xml +++ b/app/src/main/res/layout/item_message_in.xml @@ -56,6 +56,7 @@ tools:ignore="UselessParent"> @@ -71,7 +72,6 @@ tools:text="This" /> - - + android:orientation="vertical"> + + + @android:color/system_accent2_100 @android:color/system_accent2_200 + @android:color/system_accent2_300 @android:color/system_accent2_700 @android:color/system_accent2_1000 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6ed665cd..5a658924 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -39,6 +39,7 @@ #DCE1F7 #C0C6DA + #A4ABBF #414757 #F8D6FC diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 989b5d0a..64973862 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -140,4 +140,10 @@ Pin the conversation? Pin Unpin + Outgoing call + Incoming call + Ended + Cancelled + Missed + Unknown From 1288778b7b9c2d1cca23403f4de4618365e17d38 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Mon, 11 Oct 2021 00:12:11 +0300 Subject: [PATCH 11/15] fix avatars not appears --- .../com/meloda/fast/screens/messages/MessagesPreparator.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 3cf1de56..89af81ac 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 @@ -118,7 +118,8 @@ class MessagesPreparator constructor( avatar?.visibility = if (nextSenderDiff || (fiveMinAgo && prevSenderDiff) - || (!prevSenderDiff && nextMessage == null) + || !prevSenderDiff + || nextMessage == null ) View.VISIBLE else View.INVISIBLE } else { title?.isVisible = false From 63de514975d68ba45211e042f8a7110fd586a254 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Mon, 11 Oct 2021 00:18:48 +0300 Subject: [PATCH 12/15] confirm pin & unpin the message --- .../messages/MessagesHistoryFragment.kt | 35 ++++++++++++++++--- app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 32 insertions(+), 5 deletions(-) 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 7009e100..be48f2df 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 @@ -483,11 +483,12 @@ class MessagesHistoryFragment : if (attachmentController.message.value != message) attachmentController.message.value = message } - pin -> viewModel.pinMessage( - peerId = conversation.id, - messageId = message.id, - pin = !isMessageAlreadyPinned - ) + pin -> + showPinMessageDialog( + peerId = conversation.id, + messageId = message.id, + pin = !isMessageAlreadyPinned + ) edit -> { attachmentController.isEditing = true @@ -499,6 +500,30 @@ class MessagesHistoryFragment : }.show() } + private fun showPinMessageDialog( + peerId: Int, + messageId: Int?, + pin: Boolean + ) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle( + if (pin) R.string.confirm_pin_message + else R.string.confirm_unpin_message + ) + .setPositiveButton( + if (pin) R.string.action_pin + else R.string.action_unpin + ) { _, _ -> + viewModel.pinMessage( + peerId = peerId, + messageId = messageId, + pin = pin + ) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + private fun showDeleteMessageDialog(message: VkMessage) { val binding = DialogMessageDeleteBinding.inflate(layoutInflater, null, false) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64973862..8ea8a128 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -146,4 +146,6 @@ Cancelled Missed Unknown + Pin the message? + Unpin the message? From 1bba8ee4e667b43ab626451ef7a44a1e40bdf657 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Mon, 11 Oct 2021 00:33:38 +0300 Subject: [PATCH 13/15] new attachments: gift, graffiti fix NullPointerException on VkPhoto.kt text param --- .../kotlin/com/meloda/fast/api/VkUtils.kt | 8 +--- .../fast/api/model/attachments/VkGift.kt | 5 ++- .../fast/api/model/attachments/VkGraffiti.kt | 7 +++- .../fast/api/model/attachments/VkPhoto.kt | 2 +- .../api/model/base/attachments/BaseVkGift.kt | 12 +++++- .../model/base/attachments/BaseVkGraffiti.kt | 14 ++++++- .../api/model/base/attachments/BaseVkPhoto.kt | 2 +- .../screens/messages/AttachmentInflater.kt | 40 ++++++++++++++++++- .../layout/item_message_attachment_gift.xml | 16 ++++++++ .../item_message_attachment_graffiti.xml | 16 ++++++++ 10 files changed, 108 insertions(+), 14 deletions(-) create mode 100644 app/src/main/res/layout/item_message_attachment_gift.xml create mode 100644 app/src/main/res/layout/item_message_attachment_graffiti.xml 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 67dfe624..e82af0b9 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt @@ -150,9 +150,7 @@ object VkUtils { } BaseVkAttachmentItem.AttachmentType.GIFT -> { val gift = baseAttachment.gift ?: continue - attachments += VkGift( - link = gift.thumb_48 - ) + attachments += gift.asVkGift() } BaseVkAttachmentItem.AttachmentType.WALL -> { val wall = baseAttachment.wall ?: continue @@ -160,9 +158,7 @@ object VkUtils { } BaseVkAttachmentItem.AttachmentType.GRAFFITI -> { val graffiti = baseAttachment.graffiti ?: continue - attachments += VkGraffiti( - link = graffiti.url - ) + attachments += graffiti.asVkGraffiti() } BaseVkAttachmentItem.AttachmentType.POLL -> { val poll = baseAttachment.poll ?: continue diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt index 2fea4243..6be29fca 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGift.kt @@ -5,7 +5,10 @@ import kotlinx.parcelize.Parcelize @Parcelize data class VkGift( - val link: String + val id: Int, + val thumb256: String?, + val thumb96: String?, + val thumb48: String ) : VkAttachment() { @IgnoredOnParcel diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt index a9b2ca9b..9ce65371 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkGraffiti.kt @@ -5,7 +5,12 @@ import kotlinx.parcelize.Parcelize @Parcelize data class VkGraffiti( - val link: String + val id: Int, + val ownerId: Int, + val url: String, + val width: Int, + val height: Int, + val accessKey: String ) : VkAttachment() { @IgnoredOnParcel diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt index 6c3d9d56..5e008c84 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 @@ -15,7 +15,7 @@ data class VkPhoto( val hasTags: Boolean, val accessKey: String?, val sizes: List, - val text: String, + val text: String?, val userId: Int? ) : VkAttachment() { diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt index 2ed684ff..29e646b9 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGift.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.model.base.attachments import android.os.Parcelable +import com.meloda.fast.api.model.attachments.VkGift import kotlinx.parcelize.Parcelize @Parcelize @@ -9,4 +10,13 @@ data class BaseVkGift( val thumb_256: String?, val thumb_96: String?, val thumb_48: String -) : Parcelable \ No newline at end of file +) : Parcelable { + + fun asVkGift() = VkGift( + id = id, + thumb256 = thumb_256, + thumb96 = thumb_96, + thumb48 = thumb_48 + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt index da07731a..c5e841ef 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkGraffiti.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.model.base.attachments import android.os.Parcelable +import com.meloda.fast.api.model.attachments.VkGraffiti import kotlinx.parcelize.Parcelize @Parcelize @@ -11,4 +12,15 @@ data class BaseVkGraffiti( val width: Int, val height: Int, val access_key: String -) : Parcelable \ No newline at end of file +) : Parcelable { + + fun asVkGraffiti() = VkGraffiti( + id = id, + ownerId = owner_id, + url = url, + width = width, + height = height, + accessKey = access_key + ) + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt index 218f7109..babe40e4 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkPhoto.kt @@ -13,7 +13,7 @@ data class BaseVkPhoto( val has_tags: Boolean, val access_key: String?, val sizes: List, - val text: String, + val text: String?, val user_id: Int?, val lat: Double?, val long: Double?, diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt index 902748af..bb7de5a4 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 @@ -66,6 +66,10 @@ class AttachmentInflater constructor( when (val attachment = attachments[0]) { is VkSticker -> return sticker(attachment) is VkWall -> return wall(attachment) + is VkVoiceMessage -> return voice(attachment) + is VkCall -> return call(attachment) + is VkGraffiti -> return graffiti(attachment) + is VkGift -> return gift(attachment) } } @@ -94,8 +98,6 @@ class AttachmentInflater constructor( is VkAudio -> audio(attachment) is VkFile -> file(attachment) is VkLink -> link(attachment) - is VkVoiceMessage -> voice(attachment) - is VkCall -> call(attachment) else -> Log.e( "Attachment inflater", @@ -370,4 +372,38 @@ class AttachmentInflater constructor( binding.state.text = callState } + private fun graffiti(graffiti: VkGraffiti) { + val binding = ItemMessageAttachmentGraffitiBinding.inflate(inflater, container, true) + + val url = graffiti.url + + val heightCoefficient = graffiti.height / AndroidUtils.px(140) + + with(binding.image) { + layoutParams = LinearLayoutCompat.LayoutParams( + AndroidUtils.px(140).roundToInt(), + (graffiti.height / heightCoefficient).roundToInt() + ) + + load(url) { crossfade(150) } + } + } + + private fun gift(gift: VkGift) { + val binding = ItemMessageAttachmentGiftBinding.inflate(inflater, container, true) + + val url = gift.thumb256 ?: gift.thumb96 ?: gift.thumb48 + + with(binding.image) { + shapeAppearanceModel = shapeAppearanceModel.withCornerSize { AndroidUtils.px(12) } + + layoutParams = LinearLayoutCompat.LayoutParams( + AndroidUtils.px(140).roundToInt(), + AndroidUtils.px(140).roundToInt() + ) + + load(url) { crossfade(150) } + } + } + } \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_gift.xml b/app/src/main/res/layout/item_message_attachment_gift.xml new file mode 100644 index 00000000..5cc4fe57 --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_gift.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_message_attachment_graffiti.xml b/app/src/main/res/layout/item_message_attachment_graffiti.xml new file mode 100644 index 00000000..5cc4fe57 --- /dev/null +++ b/app/src/main/res/layout/item_message_attachment_graffiti.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file From b4cd69f39f5beab46939d0f75494d1a5f1fff565 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Mon, 11 Oct 2021 10:46:18 +0300 Subject: [PATCH 14/15] fix voice messages waveform and padding --- .../com/meloda/fast/screens/messages/AttachmentInflater.kt | 2 +- app/src/main/res/layout/item_message_attachment_voice.xml | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) 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 bb7de5a4..b0b7a332 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/AttachmentInflater.kt @@ -324,7 +324,7 @@ class AttachmentInflater constructor( if (message.isOut) binding.root.updatePadding( - bottom = AndroidUtils.px(5).roundToInt(), + bottom = AndroidUtils.px(6).roundToInt(), left = AndroidUtils.px(6).roundToInt() ) diff --git a/app/src/main/res/layout/item_message_attachment_voice.xml b/app/src/main/res/layout/item_message_attachment_voice.xml index 1a5452bd..2fd7d1f9 100644 --- a/app/src/main/res/layout/item_message_attachment_voice.xml +++ b/app/src/main/res/layout/item_message_attachment_voice.xml @@ -28,10 +28,11 @@ Date: Mon, 11 Oct 2021 12:08:28 +0300 Subject: [PATCH 15/15] editing messages --- .../kotlin/com/meloda/fast/api/VKConstants.kt | 13 ++ .../kotlin/com/meloda/fast/api/VkUtils.kt | 24 ++++ .../com/meloda/fast/api/model/VkMessage.kt | 45 ++----- .../api/model/attachments/VkAttachment.kt | 6 +- .../fast/api/model/attachments/VkAudio.kt | 13 +- .../fast/api/model/attachments/VkFile.kt | 13 +- .../fast/api/model/attachments/VkPhoto.kt | 9 ++ .../fast/api/model/attachments/VkVideo.kt | 13 +- .../api/model/base/attachments/BaseVkAudio.kt | 6 +- .../api/model/base/attachments/BaseVkFile.kt | 6 +- .../api/model/base/attachments/BaseVkVideo.kt | 6 +- .../com/meloda/fast/api/network/VkUrls.kt | 1 + .../network/messages/MessagesDataSource.kt | 3 + .../fast/api/network/messages/MessagesRepo.kt | 4 + .../api/network/messages/MessagesRequest.kt | 34 ++++++ .../messages/MessagesHistoryAdapter.kt | 9 ++ .../messages/MessagesHistoryFragment.kt | 111 ++++++++++++------ .../messages/MessagesHistoryViewModel.kt | 29 +++++ .../screens/messages/MessagesPreparator.kt | 2 +- .../res/drawable/ic_trash_can_outline_24.xml | 10 ++ 20 files changed, 270 insertions(+), 87 deletions(-) create mode 100644 app/src/main/res/drawable/ic_trash_can_outline_24.xml 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 ebfbd0bd..90d32bf9 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VKConstants.kt @@ -1,5 +1,7 @@ package com.meloda.fast.api +import com.meloda.fast.api.model.attachments.* + object VKConstants { const val GROUP_FIELDS = "description,members_count,counters,status,verified" @@ -35,4 +37,15 @@ object VKConstants { const val PASSWORD = "password" } } + + val restrictedToEditAttachments = listOf( + VkCall::class.java, + VkCurator::class.java, + VkEvent::class.java, + VkGift::class.java, + VkGraffiti::class.java, + VkGroupCall::class.java, + VkStory::class.java, + VkVoiceMessage::class.java + ) } \ No newline at end of file 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 e82af0b9..d446c464 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/VkUtils.kt @@ -17,6 +17,30 @@ import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem object VkUtils { + fun attachmentToString( + attachmentClass: Class, + id: Int, + ownerId: Int, + withAccessKey: Boolean, + accessKey: String? + ): String { + val type = when (attachmentClass) { + VkAudio::class.java -> "audio" + VkFile::class.java -> "doc" + VkVideo::class.java -> "video" + VkPhoto::class.java -> "photo" + else -> throw IllegalArgumentException("unknown attachment class: $attachmentClass") + } + + val result = StringBuilder(type).append(ownerId).append('_').append(id) + if (withAccessKey && !accessKey.isNullOrBlank()) { + result.append('_') + result.append(accessKey) + } + return result.toString() + } + + fun getMessageUser(message: VkMessage, profiles: Map): VkUser? { return (if (!message.isUser()) null else profiles[message.fromId]).also { message.user.value = it } 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 424c43e1..83f8d229 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/VkMessage.kt @@ -5,6 +5,7 @@ import androidx.room.Entity import androidx.room.Ignore import androidx.room.PrimaryKey import com.meloda.fast.api.UserConfig +import com.meloda.fast.api.VKConstants import com.meloda.fast.api.model.attachments.VkAttachment import com.meloda.fast.base.adapter.SelectableItem import com.meloda.fast.util.TimeUtils @@ -15,8 +16,8 @@ import kotlinx.parcelize.Parcelize @Parcelize data class VkMessage( @PrimaryKey(autoGenerate = false) - val id: Int, - val text: String? = null, + var id: Int, + var text: String? = null, val isOut: Boolean, val peerId: Int, val fromId: Int, @@ -28,7 +29,7 @@ data class VkMessage( val actionConversationMessageId: Int? = null, val actionMessage: String? = null, val geoType: String? = null, - val important: Boolean = false, + var important: Boolean = false, var forwards: List? = null, var attachments: List? = null, @@ -62,43 +63,11 @@ data class VkMessage( fun canEdit() = fromId == UserConfig.userId && + (attachments == null || !VKConstants.restrictedToEditAttachments.contains( + attachments!![0].javaClass + )) && (System.currentTimeMillis() / 1000 - date.toLong() < TimeUtils.ONE_DAY_IN_SECONDS) - fun copyMessage( - id: Int = this.id, - text: String? = this.text, - isOut: Boolean = this.isOut, - peerId: Int = this.peerId, - fromId: Int = this.fromId, - date: Int = this.date, - randomId: Int = this.randomId, - action: String? = this.action, - actionMemberId: Int? = this.actionMemberId, - actionText: String? = this.actionText, - actionConversationMessageId: Int? = this.actionConversationMessageId, - actionMessage: String? = this.actionMessage, - geoType: String? = this.geoType, - important: Boolean = this.important - ) = VkMessage( - id = id, - text = text, - isOut = isOut, - peerId = peerId, - fromId = fromId, - date = date, - randomId = randomId, - action = action, - actionMemberId = actionMemberId, - actionText = actionText, - actionConversationMessageId = actionConversationMessageId, - actionMessage = actionMessage, - geoType = geoType, - important = important - ).also { - it.attachments = attachments - it.forwards = forwards - } - enum class Action(val value: String) { CHAT_CREATE("chat_create"), CHAT_PHOTO_UPDATE("chat_photo_update"), diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt index 26d3bc1a..2662b547 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAttachment.kt @@ -4,4 +4,8 @@ import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize -open class VkAttachment : Parcelable \ No newline at end of file +open class VkAttachment : Parcelable { + + open fun asString(withAccessKey: Boolean = true) = "" + +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt index cdf4e450..f2283066 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkAudio.kt @@ -1,17 +1,28 @@ package com.meloda.fast.api.model.attachments +import com.meloda.fast.api.VkUtils import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize data class VkAudio( val id: Int, + val ownerId: Int, val title: String, val artist: String, val url: String, - val duration: Int + val duration: Int, + val accessKey: String? ) : VkAttachment() { @IgnoredOnParcel val className: String = this::class.java.name + + override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString( + attachmentClass = this::class.java, + id = id, + ownerId = ownerId, + withAccessKey = withAccessKey, + accessKey = accessKey + ) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt index e1e91542..19e7f7b1 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkFile.kt @@ -1,17 +1,28 @@ package com.meloda.fast.api.model.attachments +import com.meloda.fast.api.VkUtils import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @Parcelize data class VkFile( val id: Int, + val ownerId: Int, val title: String, val ext: String, val size: Int, - val url: String + val url: String, + val accessKey: String? ) : VkAttachment() { @IgnoredOnParcel val className: String = this::class.java.name + + override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString( + attachmentClass = this::class.java, + id = id, + ownerId = ownerId, + withAccessKey = withAccessKey, + accessKey = accessKey + ) } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt index 5e008c84..8a661d4b 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkPhoto.kt @@ -1,6 +1,7 @@ package com.meloda.fast.api.model.attachments import androidx.room.Ignore +import com.meloda.fast.api.VkUtils import com.meloda.fast.api.model.base.attachments.BaseVkPhoto import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -39,6 +40,14 @@ data class VkPhoto( @IgnoredOnParcel val className: String = this::class.java.name + override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString( + attachmentClass = this::class.java, + id = id, + ownerId = ownerId, + withAccessKey = withAccessKey, + accessKey = accessKey + ) + fun getMaxSize(): BaseVkPhoto.Size? { return getSizeOrSmaller(sizesChars.peek()) } diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVideo.kt b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVideo.kt index 33424408..b817897c 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVideo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/attachments/VkVideo.kt @@ -1,5 +1,6 @@ package com.meloda.fast.api.model.attachments +import com.meloda.fast.api.VkUtils import com.meloda.fast.api.model.base.attachments.BaseVkVideo import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -7,8 +8,10 @@ import kotlinx.parcelize.Parcelize @Parcelize data class VkVideo( val id: Int, + val ownerId: Int, val images: List, - val firstFrames: List? + val firstFrames: List?, + val accessKey: String? ) : VkAttachment() { @IgnoredOnParcel @@ -18,4 +21,12 @@ data class VkVideo( return images.find { it.width == width } } + override fun asString(withAccessKey: Boolean) = VkUtils.attachmentToString( + attachmentClass = this::class.java, + id = id, + ownerId = ownerId, + withAccessKey = withAccessKey, + accessKey = accessKey + ) + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt index a497bd44..09de47b9 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkAudio.kt @@ -13,7 +13,7 @@ data class BaseVkAudio( val url: String, val date: Int, val owner_id: Int, - val access_key: String, + val access_key: String?, val is_explicit: Boolean, val is_focus_track: Boolean, val is_licensed: Boolean, @@ -27,10 +27,12 @@ data class BaseVkAudio( fun asVkAudio() = VkAudio( id = id, + ownerId = owner_id, title = title, artist = artist, url = url, - duration = duration + duration = duration, + accessKey = access_key ) @Parcelize diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt index 07b2a967..8c09507f 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkFile.kt @@ -16,16 +16,18 @@ data class BaseVkFile( val url: String, val preview: Preview?, val ic_licensed: Int, - val access_key: String, + val access_key: String?, val web_preview_url: String? ) : BaseVkAttachment() { fun asVkFile() = VkFile( id = id, + ownerId = owner_id, title = title, ext = ext, url = url, - size = size + size = size, + accessKey = access_key ) @Parcelize diff --git a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt index c659c095..258e7a97 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/model/base/attachments/BaseVkVideo.kt @@ -26,7 +26,7 @@ data class BaseVkVideo( val can_add_to_faves: Int, val can_add: Int, val can_attach_link: Int, - val access_key: String, + val access_key: String?, val owner_id: Int, val ov_id: String, val is_favorite: Boolean, @@ -40,8 +40,10 @@ data class BaseVkVideo( fun asVkVideo() = VkVideo( id = id, + ownerId = owner_id, images = image, - firstFrames = first_frame + firstFrames = first_frame, + accessKey = access_key ) @Parcelize diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt b/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt index 60f8e3dd..dcbcb39c 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/VkUrls.kt @@ -31,6 +31,7 @@ object VkUrls { const val Pin = "$API/messages.pin" const val Unpin = "$API/messages.unpin" const val Delete = "$API/messages.delete" + const val Edit = "$API/messages.edit" } diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt index 479a9392..1fee173b 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesDataSource.kt @@ -30,6 +30,9 @@ class MessagesDataSource @Inject constructor( suspend fun delete(params: MessagesDeleteRequest) = repo.delete(params.map) + suspend fun edit(params: MessagesEditRequest) = + repo.edit(params.map) + suspend fun store(messages: List) = dao.insert(messages) suspend fun getCached(peerId: Int) = dao.getByPeerId(peerId) diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt index 4732ed19..729414c9 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRepo.kt @@ -39,4 +39,8 @@ interface MessagesRepo { @POST(VkUrls.Messages.Delete) suspend fun delete(@FieldMap params: Map): Answer> + @FormUrlEncoded + @POST(VkUrls.Messages.Edit) + suspend fun edit(@FieldMap params: Map): Answer> + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt index a0e46a17..a96918cd 100644 --- a/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt +++ b/app/src/main/kotlin/com/meloda/fast/api/network/messages/MessagesRequest.kt @@ -2,6 +2,7 @@ package com.meloda.fast.api.network.messages import android.os.Parcelable import com.meloda.fast.api.ApiExtensions.intString +import com.meloda.fast.api.model.attachments.VkAttachment import kotlinx.parcelize.Parcelize @Parcelize @@ -130,5 +131,38 @@ data class MessagesDeleteRequest( this["conversation_message_ids"] = it.joinToString { id -> id.toString() } } } +} + +@Parcelize +data class MessagesEditRequest( + val peerId: Int, + val messageId: Int, + val message: String? = null, + val lat: Float? = null, + val lon: Float? = null, + val attachments: List? = null, + val notParseLinks: Boolean = false, + val keepSnippets: Boolean = true, + val keepForwardedMessages: Boolean = true +) : Parcelable { + + val map + get() = mutableMapOf( + "peer_id" to peerId.toString(), + "message_id" to messageId.toString(), + "dont_parse_links" to notParseLinks.intString, + "keep_snippets" to keepSnippets.intString, + "keep_forward_messages" to keepForwardedMessages.intString + ).apply { + message?.let { this["message"] = it } + lat?.let { this["lat"] = it.toString() } + lon?.let { this["lon"] = it.toString() } + attachments?.let { + val attachments = + if (it.isEmpty()) "" + else it.joinToString(separator = ",") { attachment -> attachment.asString() } + this["attachment"] = attachments + } + } } \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt index 426b8a24..038b4a25 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryAdapter.kt @@ -267,6 +267,15 @@ class MessagesHistoryAdapter constructor( return positions } + fun searchMessageIndex(messageId: Int): Int? { + for (i in values.indices) { + val message = values[i] + if (message.id == messageId) return i + } + + return null + } + companion object { private const val SERVICE = 1 private const val HEADER = 0 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 be48f2df..b4242f11 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryFragment.kt @@ -51,7 +51,7 @@ class MessagesHistoryFragment : private val action = MutableLiveData() private enum class Action { - RECORD, SEND, EDIT + RECORD, SEND, EDIT, DELETE } private val user: VkUser? by lazy { @@ -167,12 +167,11 @@ class MessagesHistoryFragment : }) binding.message.doAfterTextChanged { - val canSend = - it.toString().isNotBlank() + val canSend = it.toString().isNotBlank() - val newValue = + val newValue: Action = when { - attachmentController.isEditing -> Action.EDIT + attachmentController.isEditing -> if (it.isNullOrBlank()) Action.DELETE else Action.EDIT canSend -> Action.SEND else -> Action.RECORD } @@ -203,11 +202,16 @@ class MessagesHistoryFragment : Action.EDIT -> { binding.action.setImageResource(R.drawable.ic_round_done_24) } + Action.DELETE -> { + binding.action.setImageResource(R.drawable.ic_trash_can_outline_24) + } else -> return@observe } } attachmentController.isPanelVisible.observe(viewLifecycleOwner) { + if (it) binding.message.setSelection(binding.message.text.toString().length) + val layoutParams = binding.refreshLayout.layoutParams as CoordinatorLayout.LayoutParams layoutParams.bottomMargin = if (it) (binding.attachmentPanel.height / 1.5).roundToInt() else 0 @@ -280,39 +284,58 @@ class MessagesHistoryFragment : } 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 + when (action.value) { + Action.RECORD -> { + } + Action.SEND -> { + val messageText = binding.message.text.toString().trim() + if (messageText.isBlank()) return - val date = System.currentTimeMillis() + val date = System.currentTimeMillis() - var message = VkMessage( - id = -1, - text = messageText, - isOut = true, - peerId = conversation.id, - fromId = UserConfig.userId, - date = (date / 1000).toInt(), - randomId = 0, - replyMessage = attachmentController.message.value - ) + val message = VkMessage( + id = -1, + text = messageText, + isOut = true, + peerId = conversation.id, + fromId = UserConfig.userId, + date = (date / 1000).toInt(), + randomId = 0, + replyMessage = attachmentController.message.value + ) - adapter.add(message) - adapter.notifyItemInserted(adapter.actualSize - 1) - binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) - binding.message.clear() + adapter.add(message) + adapter.notifyItemInserted(adapter.actualSize - 1) + binding.recyclerView.smoothScrollToPosition(adapter.lastPosition) + binding.message.clear() - val replyMessage = attachmentController.message.value - attachmentController.message.value = null + val replyMessage = attachmentController.message.value + attachmentController.message.value = null - viewModel.sendMessage( - peerId = conversation.id, - message = messageText, - randomId = 0, - replyTo = replyMessage?.id - ) { message = message.copyMessage(id = it) } + viewModel.sendMessage( + peerId = conversation.id, + message = messageText, + randomId = 0, + replyTo = replyMessage?.id + ) { message.id = it } + } + Action.EDIT -> { + val message = attachmentController.message.value ?: return + val messageText = binding.message.text.toString().trim() + + attachmentController.message.value = null + + viewModel.editMessage( + originalMessage = message, + peerId = conversation.id, + messageId = message.id, + message = messageText, + attachments = message.attachments + ) + } + Action.DELETE -> attachmentController.message.value?.let { + showDeleteMessageDialog(it) + } } } @@ -328,6 +351,7 @@ class MessagesHistoryFragment : is MessagesPin -> conversation.pinnedMessage = event.message is MessagesUnpin -> conversation.pinnedMessage = null is MessagesDelete -> deleteMessages(event) + is MessagesEdit -> editMessage(event) } } @@ -377,14 +401,13 @@ class MessagesHistoryFragment : for (i in adapter.values.indices) { val message = adapter.values[i] + message.important = event.important if (event.messagesIds.contains(message.id)) { if (!changed) changed = true positions.add(i) - adapter.values[i] = message.copyMessage( - important = event.important - ) + adapter.values[i] = message } } @@ -532,17 +555,22 @@ class MessagesHistoryFragment : else R.string.message_mark_as_spam ) - binding.check.isEnabled = !message.isOut || message.canEdit() + binding.check.isEnabled = + (conversation.id != UserConfig.userId) && (!message.isOut || message.canEdit()) + + if (conversation.id == UserConfig.userId) binding.check.isChecked = true MaterialAlertDialogBuilder(requireContext()) .setTitle(R.string.confirm_delete_message) .setView(binding.root) .setPositiveButton(R.string.action_delete) { _, _ -> + attachmentController.message.value = null + viewModel.deleteMessage( peerId = conversation.id, messagesIds = listOf(message.id), isSpam = if (message.isOut) null else binding.check.isChecked, - deleteForAll = if (!message.isOut || !message.canEdit()) null else binding.check.isChecked + deleteForAll = if (!binding.check.isEnabled) null else binding.check.isChecked ) } .setNegativeButton(android.R.string.cancel, null) @@ -555,6 +583,13 @@ class MessagesHistoryFragment : } } + private fun editMessage(event: MessagesEdit) { + adapter.searchMessageIndex(event.message.id)?.let { index -> + adapter.values[index] = event.message + adapter.notifyItemChanged(index) + } + } + private inner class AttachmentPanelController { val isPanelVisible = MutableLiveData(false) val message = MutableLiveData() 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 6f76f407..a4c07e50 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesHistoryViewModel.kt @@ -6,6 +6,7 @@ import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkUser +import com.meloda.fast.api.model.attachments.VkAttachment import com.meloda.fast.api.network.messages.* import com.meloda.fast.base.viewmodel.BaseViewModel import com.meloda.fast.base.viewmodel.VkEvent @@ -173,6 +174,31 @@ class MessagesHistoryViewModel @Inject constructor( ) }, onAnswer = { sendEvent(MessagesDelete(messagesIds = messagesIds ?: listOf())) }) } + + fun editMessage( + originalMessage: VkMessage, + peerId: Int, + messageId: Int, + message: String? = null, + attachments: List? = null + ) = viewModelScope.launch { + makeJob( + { + messages.edit( + MessagesEditRequest( + peerId = peerId, + messageId = messageId, + message = message, + attachments = attachments + ) + ) + }, + onAnswer = { + originalMessage.text = message + sendEvent(MessagesEdit(originalMessage)) + } + ) + } } data class MessagesLoaded( @@ -198,3 +224,6 @@ data class MessagesDelete( val messagesIds: List ) : VkEvent() +data class MessagesEdit( + val message: VkMessage +) : VkEvent() \ No newline at end of file diff --git a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt index 89af81ac..b5bfc38e 100644 --- a/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt +++ b/app/src/main/kotlin/com/meloda/fast/screens/messages/MessagesPreparator.kt @@ -213,7 +213,7 @@ class MessagesPreparator constructor( } else { text.isVisible = true bubble.isVisible = true - text.text = VkUtils.prepareMessageText(message.text) + text.text = VkUtils.prepareMessageText(message.text ?: "") } } } diff --git a/app/src/main/res/drawable/ic_trash_can_outline_24.xml b/app/src/main/res/drawable/ic_trash_can_outline_24.xml new file mode 100644 index 00000000..05862a21 --- /dev/null +++ b/app/src/main/res/drawable/ic_trash_can_outline_24.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file