From c50d38e85c08ebf9d12425876c45f3286ce20c39 Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Thu, 3 Oct 2024 11:51:33 +1000 Subject: [PATCH] Handling deletion od "marked as deleted" messages --- .../conversation/v2/ConversationActivityV2.kt | 23 ++++++----- .../conversation/v2/ConversationAdapter.kt | 13 ++++-- .../v2/ConversationReactionOverlay.kt | 35 +++++++++++----- .../conversation/v2/ConversationViewModel.kt | 40 ++++++++++--------- .../v2/messages/ControlMessageView.kt | 13 +++++- .../v2/messages/VisibleMessageView.kt | 7 +++- 6 files changed, 88 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 255ea08a0b..434fa4a3cf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -345,7 +345,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe ) { showConversationReaction(message, view) } else { - handleLongPress(message, position) + selectMessage(message, position) } }, onDeselect = { message, position -> @@ -1290,7 +1290,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } // `position` is the adapter position; not the visual position - private fun handleLongPress(message: MessageRecord, position: Int) { + private fun selectMessage(message: MessageRecord, position: Int) { val actionMode = this.actionMode val actionModeCallback = ConversationActionModeCallback(adapter, viewModel.threadId, this) actionModeCallback.delegate = this @@ -1308,15 +1308,20 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } } - private fun showConversationReaction(message: MessageRecord, visibleMessageView: VisibleMessageView) { + private fun showConversationReaction(message: MessageRecord, messageView: View) { + val messageContentView = when(messageView){ + is VisibleMessageView -> messageView.messageContentView + else -> messageView + } + val messageContentBitmap = try { - visibleMessageView.messageContentView.drawToBitmap() + messageContentView.drawToBitmap() } catch (e: Exception) { Log.e("Loki", "Failed to show emoji picker", e) return } emojiPickerVisible = true - ViewUtil.hideKeyboard(this, visibleMessageView) + ViewUtil.hideKeyboard(this, messageView) binding.reactionsShade.isVisible = true binding.scrollToBottomButton.isVisible = false binding.conversationRecyclerView.suppressLayout(true) @@ -1338,14 +1343,14 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } }) - val topLeft = intArrayOf(0, 0).also { visibleMessageView.messageContentView.getLocationInWindow(it) } + val topLeft = intArrayOf(0, 0).also { messageContentView.getLocationInWindow(it) } val selectedConversationModel = SelectedConversationModel( messageContentBitmap, topLeft[0].toFloat(), topLeft[1].toFloat(), - visibleMessageView.messageContentView.width, + messageContentView.width, message.isOutgoing, - visibleMessageView.messageContentView + messageContentView ) reactionDelegate.show(this, message, selectedConversationModel, viewModel.blindedPublicKey) } @@ -2056,7 +2061,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } override fun selectMessages(messages: Set) { - handleLongPress(messages.first(), 0) //TODO: begin selection mode + selectMessage(messages.first(), 0) //TODO: begin selection mode } // Note: The messages in the provided set may be a single message, or multiple if there are a diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index 83e7dcfa92..c83f71074d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -5,6 +5,7 @@ import android.database.Cursor import android.util.SparseArray import android.util.SparseBooleanArray import android.view.MotionEvent +import android.view.View import android.view.ViewGroup import androidx.annotation.WorkerThread import androidx.core.util.getOrDefault @@ -35,7 +36,7 @@ class ConversationAdapter( private val isReversed: Boolean, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit, private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, - private val onItemLongPress: (MessageRecord, Int, VisibleMessageView) -> Unit, + private val onItemLongPress: (MessageRecord, Int, View) -> Unit, private val onDeselect: (MessageRecord, Int) -> Unit, private val onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit, private val glide: RequestManager, @@ -156,12 +157,18 @@ class ConversationAdapter( } else { visibleMessageView.onPress = null visibleMessageView.onSwipeToReply = null - visibleMessageView.onLongPress = null + // you can long press on "marked as deleted" messages + visibleMessageView.onLongPress = + { onItemLongPress(message, viewHolder.adapterPosition, visibleMessageView) } } } is ControlMessageViewHolder -> { - viewHolder.view.bind(message, messageBefore) + viewHolder.view.bind( + message = message, + previous = messageBefore, + longPress = { onItemLongPress(message, viewHolder.adapterPosition, viewHolder.view) } + ) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt index d445d002cb..1e000dd4ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.kt @@ -21,6 +21,7 @@ import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.ContextCompat import androidx.core.view.doOnLayout +import androidx.core.view.isVisible import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat import com.squareup.phrase.Phrase import dagger.hilt.android.AndroidEntryPoint @@ -527,18 +528,25 @@ class ConversationReactionOverlay : FrameLayout { ?: return emptyList() val userPublicKey = getLocalNumber(context)!! // Select message - items += ActionItem(R.attr.menu_select_icon, R.string.select, { handleActionItemClicked(Action.SELECT) }, R.string.AccessibilityId_select) + if(!message.isDeleted) { + items += ActionItem( + R.attr.menu_select_icon, + R.string.select, + { handleActionItemClicked(Action.SELECT) }, + R.string.AccessibilityId_select + ) + } // Reply val canWrite = openGroup == null || openGroup.canWrite - if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation) { + if (canWrite && !message.isPending && !message.isFailed && !message.isOpenGroupInvitation && !message.isDeleted) { items += ActionItem(R.attr.menu_reply_icon, R.string.reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply) } // Copy message text - if (!containsControlMessage && hasText) { + if (!containsControlMessage && hasText && !message.isDeleted) { items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) }) } // Copy Account ID - if (!recipient.isCommunityRecipient && message.isIncoming) { + if (!recipient.isCommunityRecipient && message.isIncoming && !message.isDeleted) { items += ActionItem(R.attr.menu_copy_icon, R.string.accountIDCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) }) } // Delete message @@ -547,15 +555,20 @@ class ConversationReactionOverlay : FrameLayout { R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger)) } // Ban user - if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) { + if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey) && !message.isDeleted) { items += ActionItem(R.attr.menu_block_icon, R.string.banUser, { handleActionItemClicked(Action.BAN_USER) }) } // Ban and delete all - if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey)) { + if (userCanBanSelectedUsers(context, message, openGroup, userPublicKey, blindedPublicKey) && !message.isDeleted) { items += ActionItem(R.attr.menu_trash_icon, R.string.banDeleteAll, { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) }) } // Message detail - items += ActionItem(R.attr.menu_info_icon, R.string.messageInfo, { handleActionItemClicked(Action.VIEW_INFO) }) + if(!message.isDeleted) { + items += ActionItem( + R.attr.menu_info_icon, + R.string.messageInfo, + { handleActionItemClicked(Action.VIEW_INFO) }) + } // Resend if (message.isFailed) { items += ActionItem(R.attr.menu_reply_icon, R.string.resend, { handleActionItemClicked(Action.RESEND) }) @@ -565,7 +578,7 @@ class ConversationReactionOverlay : FrameLayout { items += ActionItem(R.attr.menu_reply_icon, R.string.resync, { handleActionItemClicked(Action.RESYNC) }) } // Save media.. - if (message.isMms) { + if (message.isMms && !message.isDeleted) { // ..but only provide the save option if the there is a media attachment which has finished downloading. val mmsMessage = message as MediaMmsMessageRecord if (mmsMessage.containsMediaSlide() && !mmsMessage.isMediaPending) { @@ -576,8 +589,10 @@ class ConversationReactionOverlay : FrameLayout { ) } } - backgroundView.visibility = VISIBLE - foregroundView.visibility = VISIBLE + + // deleted messages have no emoji reactions + backgroundView.isVisible = !message.isDeleted + foregroundView.isVisible = !message.isDeleted return items } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index be7835a1aa..84323b14cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -245,12 +245,11 @@ class ConversationViewModel( val conversationType = conversation.getType() // hashes are required if wanting to delete messages from the 'storage server' - they are not required for communities - val canDeleteForEveryone = conversationType == MessageType.COMMUNITY || messages.all { - lokiMessageDb.getMessageServerHash( - it.id, - it.isMms - ) != null - } + // also we can only delete deleted messages (marked as deleted) locally + val canDeleteForEveryone = messages.all{ !it.isDeleted } && ( + conversationType == MessageType.COMMUNITY || + messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null + }) // There are three types of dialogs for deletion: // 1- Delete on device only OR all devices - Used for Note to self @@ -290,7 +289,6 @@ class ConversationViewModel( // for non admins, users interacting with someone else's message, or control messages else -> { - //todo DELETION this should also happen for ControlMessages _dialogsState.update { it.copy(deleteDeviceOnly = messages) } @@ -300,23 +298,29 @@ class ConversationViewModel( } /** - * This will mark the messages as deleted, locally only. - * Attachments and other related data will be removed from the db, - * but the messages themselves won't be removed from the db. - * Instead they will appear as a special type of message + * This delete the message locally only. + * Attachments and other related data will be removed from the db. + * If the messages were already marked as deleted they will be removed fully from the db, + * otherwise they will appear as a special type of message * that says something like "This message was deleted" */ - fun markAsDeletedLocally(messages: Set) { + fun deletedLocally(messages: Set) { // make sure to stop audio messages, if any messages.filterIsInstance() .mapNotNull { it.slideDeck.audioSlide } .forEach(::stopMessageAudio) - - repository.markAsDeletedLocally( - messages = messages, - displayedMessage = application.getString(R.string.deleteMessageDeletedLocally) - ) + // if the message was already marked as deleted, remove it from the db instead + if(messages.all { it.isDeleted }){ + // Remove the message locally (leave nothing behind) + repository.deleteMessages(messages = messages, threadId = threadId) + } else { + // only mark as deleted (message remains behind with "This message was deleted on this device" ) + repository.markAsDeletedLocally( + messages = messages, + displayedMessage = application.getString(R.string.deleteMessageDeletedLocally) + ) + } // show confirmation toast Toast.makeText( @@ -730,7 +734,7 @@ class ConversationViewModel( it.copy(deleteDeviceOnly = null) } - markAsDeletedLocally(command.messages) + deletedLocally(command.messages) } is Commands.MarkAsDeletedForEveryone -> { markAsDeletedForEveryone(command.data) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt index 1a7040b031..c633acc839 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/ControlMessageView.kt @@ -58,7 +58,7 @@ class ControlMessageView : LinearLayout { layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) } - fun bind(message: MessageRecord, previous: MessageRecord?) { + fun bind(message: MessageRecord, previous: MessageRecord?, longPress: (() -> Unit)? = null) { binding.dateBreakTextView.showDateBreak(message, previous) binding.iconImageView.isGone = true binding.expirationTimerView.isGone = true @@ -199,6 +199,17 @@ class ControlMessageView : LinearLayout { binding.textView.isGone = message.isCallLog binding.callView.isVisible = message.isCallLog + + // handle long clicked if it was passed on + //todo DELETION currently control messages lose their ability to be clickable due to the long click, like the "mised phone call" CM + Log.d("", "*** Has long click? $longPress") + longPress?.let { + binding.root.setOnLongClickListener { + Log.d("", "*** Long clicking") + longPress.invoke() + true + } + } } fun showInfo(){ diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index bacf845ddb..0176ea5dbd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -485,10 +485,13 @@ class VisibleMessageView : FrameLayout { // region Interaction @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { - if (onPress == null || onSwipeToReply == null || onLongPress == null) { return false } + if (onPress == null && onSwipeToReply == null && onLongPress == null) { return false } when (event.action) { MotionEvent.ACTION_DOWN -> onDown(event) - MotionEvent.ACTION_MOVE -> onMove(event) + MotionEvent.ACTION_MOVE -> { + // only bother with movements if we have swipe to reply + onSwipeToReply?.let { onMove(event) } + } MotionEvent.ACTION_CANCEL -> onCancel(event) MotionEvent.ACTION_UP -> onUp(event) }