delete dialogs

delete messages (+ for all & mark as spam)
This commit is contained in:
2021-10-10 00:40:30 +03:00
parent 7c1a7d8a89
commit 074400daab
19 changed files with 392 additions and 95 deletions
@@ -0,0 +1,8 @@
package com.meloda.fast.api
object ApiExtensions {
val Boolean.intString get() = (if (this) 1 else 0).toString()
}
@@ -7,6 +7,7 @@ import android.text.SpannableString
import android.text.style.StyleSpan import android.text.style.StyleSpan
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.meloda.fast.R 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.VkGroup
import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser import com.meloda.fast.api.model.VkUser
@@ -16,6 +17,64 @@ import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
object VkUtils { object VkUtils {
fun getMessageUser(message: VkMessage, profiles: Map<Int, VkUser>): VkUser? {
return (if (!message.isUser()) null
else profiles[message.fromId]).also { message.user.value = it }
}
fun getMessageGroup(message: VkMessage, groups: Map<Int, VkGroup>): 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<Int, VkUser>): VkUser? {
return (if (!conversation.isUser()) null
else profiles[conversation.id]).also { conversation.user.value = it }
}
fun getConversationGroup(conversation: VkConversation, groups: Map<Int, VkGroup>): 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 { fun prepareMessageText(text: String, forConversations: Boolean? = null): String {
return text.apply { return text.apply {
if (forConversations == true) replace("\n", "") if (forConversations == true) replace("\n", "")
@@ -1,9 +1,12 @@
package com.meloda.fast.api.model package com.meloda.fast.api.model
import android.os.Parcelable import android.os.Parcelable
import androidx.lifecycle.MutableLiveData
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Entity import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Entity(tableName = "conversations") @Entity(tableName = "conversations")
@@ -34,6 +37,14 @@ data class VkConversation(
var lastMessage: VkMessage? = null, var lastMessage: VkMessage? = null,
) : Parcelable { ) : Parcelable {
@Ignore
@IgnoredOnParcel
val user = MutableLiveData<VkUser?>()
@Ignore
@IgnoredOnParcel
val group = MutableLiveData<VkGroup?>()
fun isChat() = type == "chat" fun isChat() = type == "chat"
fun isUser() = type == "user" fun isUser() = type == "user"
fun isGroup() = type == "group" fun isGroup() = type == "group"
@@ -23,4 +23,9 @@ data class ConversationsGetRequest(
extended?.let { this["extended"] = it.toString() } extended?.let { this["extended"] = it.toString() }
startMessageId?.let { this["start_message_id"] = 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())
} }
@@ -1,6 +1,7 @@
package com.meloda.fast.api.model.request package com.meloda.fast.api.model.request
import android.os.Parcelable import android.os.Parcelable
import com.meloda.fast.api.ApiExtensions.intString
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
@@ -20,9 +21,9 @@ data class MessagesGetHistoryRequest(
).apply { ).apply {
count?.let { this["count"] = it.toString() } count?.let { this["count"] = it.toString() }
offset?.let { this["offset"] = 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() } 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 } fields?.let { this["fields"] = it }
} }
@@ -51,8 +52,8 @@ data class MessagesSendRequest(
lon?.let { this["lon"] = it.toString() } lon?.let { this["lon"] = it.toString() }
replyTo?.let { this["reply_to"] = it.toString() } replyTo?.let { this["reply_to"] = it.toString() }
stickerId?.let { this["sticker_id"] = it.toString() } stickerId?.let { this["sticker_id"] = it.toString() }
disableMentions?.let { this["disable_mentions"] = (if (it) 1 else 0).toString() } disableMentions?.let { this["disable_mentions"] = it.intString }
dontParseLinks?.let { this["dont_parse_links"] = (if (it) 1 else 0).toString() } dontParseLinks?.let { this["dont_parse_links"] = it.intString }
} }
} }
@@ -65,11 +66,25 @@ data class MessagesMarkAsImportantRequest(
val map val map
get() = mutableMapOf( get() = mutableMapOf(
"message_ids" to messagesIds.joinToString { it.toString() }, "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 @Parcelize
data class MessagesPinMessageRequest( data class MessagesPinMessageRequest(
val peerId: Int, val peerId: Int,
@@ -93,14 +108,27 @@ data class MessagesUnPinMessageRequest(val peerId: Int) : Parcelable {
} }
@Parcelize @Parcelize
data class MessagesGetLongPollServerRequest( data class MessagesDeleteRequest(
val needPts: Boolean, val peerId: Int,
val version: Int val messagesIds: List<Int>? = null,
val conversationsMessagesIds: List<Int>? = null,
val isSpam: Boolean? = null,
val deleteForAll: Boolean? = null
) : Parcelable { ) : Parcelable {
val map val map
get() = mutableMapOf( get() = mutableMapOf(
"need_pts" to (if (needPts) 1 else 0).toString(), "peer_id" to peerId.toString()
"version" to version.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() }
}
}
} }
@@ -12,6 +12,7 @@ object VkUrls {
object Conversations { object Conversations {
const val Get = "$API/messages.getConversations" const val Get = "$API/messages.getConversations"
const val Delete = "$API/messages.deleteConversation"
} }
object Users { object Users {
@@ -22,10 +23,11 @@ object VkUrls {
const val GetHistory = "$API/messages.getHistory" const val GetHistory = "$API/messages.getHistory"
const val Send = "$API/messages.send" const val Send = "$API/messages.send"
const val MarkAsImportant = "$API/messages.markAsImportant" 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 GetLongPollServer = "$API/messages.getLongPollServer"
const val GetLongPollHistory = "$API/messages.getLongPollHistory" const val GetLongPollHistory = "$API/messages.getLongPollHistory"
const val Pin = "$API/messages.pin"
const val Unpin = "$API/messages.unpin"
const val Delete = "$API/messages.delete"
} }
@@ -1,8 +1,9 @@
package com.meloda.fast.api.network.datasource package com.meloda.fast.api.network.datasource
import com.meloda.fast.api.model.VkConversation 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.model.request.ConversationsGetRequest
import com.meloda.fast.api.network.repo.ConversationsRepo
import com.meloda.fast.database.dao.ConversationsDao import com.meloda.fast.database.dao.ConversationsDao
import javax.inject.Inject import javax.inject.Inject
@@ -11,8 +12,10 @@ class ConversationsDataSource @Inject constructor(
private val dao: ConversationsDao 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<VkConversation>) = dao.insert(conversations) suspend fun delete(params: ConversationsDeleteRequest) = repo.delete(params.map)
suspend fun store(conversations: List<VkConversation>) = dao.insert(conversations)
} }
@@ -29,6 +29,9 @@ class MessagesDataSource @Inject constructor(
suspend fun unpin(params: MessagesUnPinMessageRequest) = suspend fun unpin(params: MessagesUnPinMessageRequest) =
repo.unpin(params.map) repo.unpin(params.map)
suspend fun delete(params: MessagesDeleteRequest) =
repo.delete(params.map)
suspend fun store(messages: List<VkMessage>) = dao.insert(messages) suspend fun store(messages: List<VkMessage>) = dao.insert(messages)
suspend fun getCached(peerId: Int) = dao.getByPeerId(peerId) suspend fun getCached(peerId: Int) = dao.getByPeerId(peerId)
@@ -1,9 +1,9 @@
package com.meloda.fast.api.network.repo package com.meloda.fast.api.network.repo
import com.meloda.fast.api.base.ApiResponse 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.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.FieldMap
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST import retrofit2.http.POST
@@ -12,6 +12,10 @@ interface ConversationsRepo {
@FormUrlEncoded @FormUrlEncoded
@POST(VkUrls.Conversations.Get) @POST(VkUrls.Conversations.Get)
suspend fun getAllChats(@FieldMap params: Map<String, String>): Answer<ApiResponse<ConversationsGetResponse>> suspend fun get(@FieldMap params: Map<String, String>): Answer<ApiResponse<ConversationsGetResponse>>
@FormUrlEncoded
@POST(VkUrls.Conversations.Delete)
suspend fun delete(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
} }
@@ -36,4 +36,8 @@ interface MessagesRepo {
@POST(VkUrls.Messages.Unpin) @POST(VkUrls.Messages.Unpin)
suspend fun unpin(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>> suspend fun unpin(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
@FormUrlEncoded
@POST(VkUrls.Messages.Delete)
suspend fun delete(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
} }
@@ -75,29 +75,17 @@ class ConversationsAdapter constructor(
return return
} }
val chatUser: VkUser? = if (conversation.isUser()) { val conversationUser = VkUtils.getConversationUser(conversation, profiles)
profiles[conversation.id] val conversationGroup = VkUtils.getConversationGroup(conversation, groups)
} else null
val messageUser: VkUser? = if (message.isUser()) { val messageUser = VkUtils.getMessageUser(message, profiles)
profiles[message.fromId] val messageGroup = VkUtils.getMessageGroup(message, groups)
} else null
val chatGroup: VkGroup? = if (conversation.isGroup()) { val avatar = VkUtils.getConversationAvatar(
groups[conversation.id] conversation = conversation,
} else null conversationUser = conversationUser,
conversationGroup = conversationGroup
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
}
binding.avatar.isVisible = avatar != null 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 binding.pin.isVisible = conversation.isPinned
@@ -210,7 +198,8 @@ class ConversationsAdapter constructor(
binding.message.text = spanMessage binding.message.text = spanMessage
binding.title.text = 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) 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 { companion object {
private val COMPARATOR = object : DiffUtil.ItemCallback<VkConversation>() { private val COMPARATOR = object : DiffUtil.ItemCallback<VkConversation>() {
override fun areItemsTheSame( override fun areItemsTheSame(
@@ -188,9 +188,11 @@ class ConversationsFragment :
override fun onEvent(event: VkEvent) { override fun onEvent(event: VkEvent) {
super.onEvent(event) super.onEvent(event)
when (event) { when (event) {
is ConversationsLoaded -> refreshConversations(event)
is StartProgressEvent -> onProgressStarted() is StartProgressEvent -> onProgressStarted()
is StopProgressEvent -> onProgressStopped() is StopProgressEvent -> onProgressStopped()
is ConversationsLoaded -> refreshConversations(event)
is ConversationsDelete -> deleteConversation(event.peerId)
} }
} }
@@ -263,8 +265,40 @@ class ConversationsFragment :
} }
private fun onItemLongClick(position: Int): Boolean { private fun onItemLongClick(position: Int): Boolean {
binding.createChat.performClick() showOptionsDialog(position)
return true 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)
}
} }
@@ -6,6 +6,7 @@ import com.meloda.fast.api.VKConstants
import com.meloda.fast.api.model.VkConversation import com.meloda.fast.api.model.VkConversation
import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkUser 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.ConversationsGetRequest
import com.meloda.fast.api.model.request.UsersGetRequest import com.meloda.fast.api.model.request.UsersGetRequest
import com.meloda.fast.api.network.datasource.ConversationsDataSource import com.meloda.fast.api.network.datasource.ConversationsDataSource
@@ -20,15 +21,15 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class ConversationsViewModel @Inject constructor( class ConversationsViewModel @Inject constructor(
private val dataSource: ConversationsDataSource, private val conversations: ConversationsDataSource,
private val usersDataSource: UsersDataSource private val users: UsersDataSource
) : BaseViewModel() { ) : BaseViewModel() {
fun loadConversations( fun loadConversations(
offset: Int? = null offset: Int? = null
) = viewModelScope.launch(Dispatchers.Default) { ) = viewModelScope.launch(Dispatchers.Default) {
makeJob({ makeJob({
dataSource.getAllChats( conversations.get(
ConversationsGetRequest( ConversationsGetRequest(
count = 30, count = 30,
extended = true, extended = true,
@@ -69,16 +70,24 @@ class ConversationsViewModel @Inject constructor(
} }
fun loadProfileUser() = viewModelScope.launch { fun loadProfileUser() = viewModelScope.launch {
makeJob({ usersDataSource.getById(UsersGetRequest(fields = VKConstants.USER_FIELDS)) }, makeJob({ users.getById(UsersGetRequest(fields = VKConstants.USER_FIELDS)) },
onAnswer = { onAnswer = {
it.response?.let { r -> it.response?.let { r ->
val users = r.map { u -> u.asVkUser() } val users = r.map { u -> u.asVkUser() }
usersDataSource.storeUsers(users) this@ConversationsViewModel.users.storeUsers(users)
UserConfig.vkUser.value = users[0] UserConfig.vkUser.value = users[0]
} }
}) })
} }
fun deleteConversation(peerId: Int) = viewModelScope.launch {
makeJob({
conversations.delete(
ConversationsDeleteRequest(peerId)
)
}, onAnswer = { sendEvent(ConversationsDelete(peerId)) })
}
} }
data class ConversationsLoaded( data class ConversationsLoaded(
@@ -89,3 +98,5 @@ data class ConversationsLoaded(
val profiles: HashMap<Int, VkUser>, val profiles: HashMap<Int, VkUser>,
val groups: HashMap<Int, VkGroup> val groups: HashMap<Int, VkGroup>
) : VkEvent() ) : VkEvent()
data class ConversationsDelete(val peerId: Int) : VkEvent()
@@ -5,7 +5,6 @@ import android.graphics.Color
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.widget.LinearLayoutCompat import androidx.appcompat.widget.LinearLayoutCompat
import androidx.core.view.isVisible 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.VkGroup
import com.meloda.fast.api.model.VkMessage import com.meloda.fast.api.model.VkMessage
import com.meloda.fast.api.model.VkUser 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.api.model.attachments.VkPhoto
import com.meloda.fast.base.adapter.BaseAdapter import com.meloda.fast.base.adapter.BaseAdapter
import com.meloda.fast.base.adapter.BaseHolder import com.meloda.fast.base.adapter.BaseHolder
@@ -34,11 +32,7 @@ class MessagesHistoryAdapter constructor(
val groups: HashMap<Int, VkGroup> = hashMapOf() val groups: HashMap<Int, VkGroup> = hashMapOf()
) : BaseAdapter<VkMessage, MessagesHistoryAdapter.BasicHolder>(context, values, COMPARATOR) { ) : BaseAdapter<VkMessage, MessagesHistoryAdapter.BasicHolder>(context, values, COMPARATOR) {
private var highlightTimer: Timer? = null var avatarLongClickListener: ((position: Int) -> Unit)? = null
var onItemClickListener: ((position: Int, view: View) -> Unit)? = null
var attachmentClickListener: ((attachment: VkAttachment) -> Unit)? = null
override fun getItemViewType(position: Int): Int { override fun getItemViewType(position: Int): Int {
when { when {
@@ -76,11 +70,19 @@ class MessagesHistoryAdapter constructor(
} }
} }
override fun initListeners(itemView: View, position: Int) { // override fun initListeners(itemView: View, position: Int) {
if (itemView is AdapterView<*>) return // 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 { private fun createEmptyView(size: Int) = View(context).apply {
@@ -142,6 +144,11 @@ class MessagesHistoryAdapter constructor(
).setPhotoClickListener { ).setPhotoClickListener {
Toast.makeText(context, "Photo url: $it", Toast.LENGTH_LONG).show() Toast.makeText(context, "Photo url: $it", Toast.LENGTH_LONG).show()
}.prepare() }.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 { return null
if (actualSize == 0) return 2 }
return super.getItemCount() + 2
fun removeMessagesByIds(ids: List<Int>): List<Int> {
val positions = mutableListOf<Int>()
for (i in values.indices) {
val message = values[i]
if (ids.contains(message.id)) {
values.removeAt(i)
positions += i
}
}
return positions
} }
companion object { companion object {
@@ -6,6 +6,7 @@ import android.os.Bundle
import android.text.TextUtils import android.text.TextUtils
import android.view.View import android.view.View
import android.viewbinding.library.fragment.viewBinding import android.viewbinding.library.fragment.viewBinding
import android.widget.Toast
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.isVisible 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.R
import com.meloda.fast.api.UserConfig import com.meloda.fast.api.UserConfig
import com.meloda.fast.api.VKConstants 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.VkConversation
import com.meloda.fast.api.model.VkGroup import com.meloda.fast.api.model.VkGroup
import com.meloda.fast.api.model.VkMessage 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.StartProgressEvent
import com.meloda.fast.base.viewmodel.StopProgressEvent import com.meloda.fast.base.viewmodel.StopProgressEvent
import com.meloda.fast.base.viewmodel.VkEvent import com.meloda.fast.base.viewmodel.VkEvent
import com.meloda.fast.databinding.DialogMessageDeleteBinding
import com.meloda.fast.databinding.FragmentMessagesHistoryBinding import com.meloda.fast.databinding.FragmentMessagesHistoryBinding
import com.meloda.fast.extensions.TextViewExtensions.clear import com.meloda.fast.extensions.TextViewExtensions.clear
import com.meloda.fast.util.AndroidUtils import com.meloda.fast.util.AndroidUtils
@@ -65,8 +68,9 @@ class MessagesHistoryFragment :
private val adapter: MessagesHistoryAdapter by lazy { private val adapter: MessagesHistoryAdapter by lazy {
MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also { MessagesHistoryAdapter(requireContext(), mutableListOf(), conversation).also {
it.onItemClickListener = this::onItemClick it.itemClickListener = this::onItemClick
it.itemLongClickListener = this::onItemLongClick it.itemLongClickListener = this::onItemLongClick
it.avatarLongClickListener = this::onAvatarLongClickListener
} }
} }
@@ -316,11 +320,14 @@ class MessagesHistoryFragment :
super.onEvent(event) super.onEvent(event)
when (event) { when (event) {
is StartProgressEvent -> onProgressStarted()
is StopProgressEvent -> onProgressStopped()
is MessagesMarkAsImportant -> markMessagesAsImportant(event) is MessagesMarkAsImportant -> markMessagesAsImportant(event)
is MessagesLoaded -> refreshMessages(event) is MessagesLoaded -> refreshMessages(event)
is MessagesPin -> conversation.pinnedMessage = event.message is MessagesPin -> conversation.pinnedMessage = event.message
is StartProgressEvent -> onProgressStarted() is MessagesUnpin -> conversation.pinnedMessage = null
is StopProgressEvent -> onProgressStopped() is MessagesDelete -> deleteMessages(event)
} }
} }
@@ -402,7 +409,23 @@ class MessagesHistoryFragment :
else binding.recyclerView.scrollToPosition(adapter.lastPosition) 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] val message = adapter.values[position]
if (message.action != null) return if (message.action != null) return
@@ -414,6 +437,11 @@ class MessagesHistoryFragment :
).format(message.date * 1000L) ).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 reply = getString(R.string.message_context_action_reply)
val isMessageAlreadyPinned = message.id == conversation.pinnedMessage?.id val isMessageAlreadyPinned = message.id == conversation.pinnedMessage?.id
@@ -425,27 +453,25 @@ class MessagesHistoryFragment :
val edit = getString(R.string.message_context_action_edit) val edit = getString(R.string.message_context_action_edit)
val important = getString( val delete = getString(R.string.message_context_action_delete)
if (message.important) R.string.message_context_action_unmark_as_important
else R.string.message_context_action_mark_as_important val params = mutableListOf(
important, reply
) )
val params = mutableListOf<String>()
params.add(reply)
if (conversation.canChangePin) { if (conversation.canChangePin) {
params.add(pin) params += pin
} }
if (message.canEdit()) { if (message.canEdit()) {
params.add(edit) params += edit
} }
params.add(important) params += delete
val arrayParams = params.toTypedArray() val arrayParams = params.toTypedArray()
val dialog = MaterialAlertDialogBuilder(requireContext()) MaterialAlertDialogBuilder(requireContext())
.setTitle(time) .setTitle(time)
.setItems(arrayParams) { _, which -> .setItems(arrayParams) { _, which ->
when (params[which]) { when (params[which]) {
@@ -468,16 +494,40 @@ class MessagesHistoryFragment :
if (attachmentController.message.value != message) if (attachmentController.message.value != message)
attachmentController.message.value = message attachmentController.message.value = message
} }
delete -> showDeleteMessageDialog(message)
} }
} }.show()
dialog.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 { private inner class AttachmentPanelController {
@@ -154,6 +154,26 @@ class MessagesHistoryViewModel @Inject constructor(
) )
} }
} }
fun deleteMessage(
peerId: Int,
messagesIds: List<Int>? = null,
conversationsMessagesIds: List<Int>? = 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( data class MessagesLoaded(
@@ -175,3 +195,7 @@ data class MessagesPin(
object MessagesUnpin : VkEvent() object MessagesUnpin : VkEvent()
data class MessagesDelete(
val messagesIds: List<Int>
) : VkEvent()
@@ -76,13 +76,8 @@ class MessagesPreparator constructor(
} }
fun prepare() { fun prepare() {
val messageUser: VkUser? = (if (message.isUser()) { val messageUser = VkUtils.getMessageUser(message, profiles)
profiles[message.fromId] val messageGroup = VkUtils.getMessageGroup(message, groups)
} else null).also { message.user.value = it }
val messageGroup: VkGroup? = (if (message.isGroup()) {
groups[message.fromId]
} else null).also { message.group.value = it }
prepareRootBackground() prepareRootBackground()
@@ -225,11 +220,7 @@ class MessagesPreparator constructor(
messageGroup: VkGroup? = null messageGroup: VkGroup? = null
) { ) {
if (avatar != null) { if (avatar != null) {
val avatarUrl = when { val avatarUrl = VkUtils.getMessageAvatar(message, messageUser, messageGroup)
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) } avatar.load(avatarUrl) { crossfade(100) }
} }
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp">
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/check"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/message_delete_for_all"
app:useMaterialThemeColors="true" />
</androidx.appcompat.widget.LinearLayoutCompat>
</layout>
+11
View File
@@ -121,4 +121,15 @@
<string name="message_context_action_pin">Pin</string> <string name="message_context_action_pin">Pin</string>
<string name="message_context_action_unpin">Unpin</string> <string name="message_context_action_unpin">Unpin</string>
<string name="message_context_action_edit">Edit</string> <string name="message_context_action_edit">Edit</string>
<string name="message_context_action_delete">Delete</string>
<string name="confirm_delete_message">Delete the message?</string>
<string name="message_delete_for_all">Delete for all</string>
<string name="message_mark_as_spam">Mark as spam</string>
<string name="action_delete">Delete</string>
<string name="conversation_context_action_delete">Delete</string>
<string name="confirm_delete_conversation">Delete the conversation?</string>
</resources> </resources>