editing messages

This commit is contained in:
2021-10-11 12:08:28 +03:00
parent b4cd69f39f
commit 7af9fe6da2
20 changed files with 270 additions and 87 deletions
@@ -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
)
}
@@ -17,6 +17,30 @@ import com.meloda.fast.api.model.base.attachments.BaseVkAttachmentItem
object VkUtils {
fun <T> attachmentToString(
attachmentClass: Class<T>,
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<Int, VkUser>): VkUser? {
return (if (!message.isUser()) null
else profiles[message.fromId]).also { message.user.value = it }
@@ -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<VkMessage>? = null,
var attachments: List<VkAttachment>? = 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"),
@@ -4,4 +4,8 @@ import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
open class VkAttachment : Parcelable
open class VkAttachment : Parcelable {
open fun asString(withAccessKey: Boolean = true) = ""
}
@@ -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
)
}
@@ -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
)
}
@@ -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())
}
@@ -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<BaseVkVideo.Image>,
val firstFrames: List<BaseVkVideo.FirstFrame>?
val firstFrames: List<BaseVkVideo.FirstFrame>?,
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
)
}
@@ -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
@@ -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
@@ -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
@@ -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"
}
@@ -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<VkMessage>) = dao.insert(messages)
suspend fun getCached(peerId: Int) = dao.getByPeerId(peerId)
@@ -39,4 +39,8 @@ interface MessagesRepo {
@POST(VkUrls.Messages.Delete)
suspend fun delete(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
@FormUrlEncoded
@POST(VkUrls.Messages.Edit)
suspend fun edit(@FieldMap params: Map<String, String>): Answer<ApiResponse<Any>>
}
@@ -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<VkAttachment>? = 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
}
}
}
@@ -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
@@ -51,7 +51,7 @@ class MessagesHistoryFragment :
private val action = MutableLiveData<Action>()
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<VkMessage?>()
@@ -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<VkAttachment>? = 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<Int>
) : VkEvent()
data class MessagesEdit(
val message: VkMessage
) : VkEvent()
@@ -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 ?: "")
}
}
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M9,3V4H4V6H5V19A2,2 0 0,0 7,21H17A2,2 0 0,0 19,19V6H20V4H15V3H9M7,6H17V19H7V6M9,8V17H11V8H9M13,8V17H15V8H13Z" />
</vector>