From 074400daab401171a106169bc25427670d212a3d Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Sun, 10 Oct 2021 00:40:30 +0300 Subject: [PATCH] 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?