Handling deletion od "marked as deleted" messages

pull/1518/head
ThomasSession 7 months ago
parent 331a8f2ac6
commit c50d38e85c

@ -345,7 +345,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
) { ) {
showConversationReaction(message, view) showConversationReaction(message, view)
} else { } else {
handleLongPress(message, position) selectMessage(message, position)
} }
}, },
onDeselect = { message, position -> onDeselect = { message, position ->
@ -1290,7 +1290,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
// `position` is the adapter position; not the visual position // `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 actionMode = this.actionMode
val actionModeCallback = ConversationActionModeCallback(adapter, viewModel.threadId, this) val actionModeCallback = ConversationActionModeCallback(adapter, viewModel.threadId, this)
actionModeCallback.delegate = 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 { val messageContentBitmap = try {
visibleMessageView.messageContentView.drawToBitmap() messageContentView.drawToBitmap()
} catch (e: Exception) { } catch (e: Exception) {
Log.e("Loki", "Failed to show emoji picker", e) Log.e("Loki", "Failed to show emoji picker", e)
return return
} }
emojiPickerVisible = true emojiPickerVisible = true
ViewUtil.hideKeyboard(this, visibleMessageView) ViewUtil.hideKeyboard(this, messageView)
binding.reactionsShade.isVisible = true binding.reactionsShade.isVisible = true
binding.scrollToBottomButton.isVisible = false binding.scrollToBottomButton.isVisible = false
binding.conversationRecyclerView.suppressLayout(true) 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( val selectedConversationModel = SelectedConversationModel(
messageContentBitmap, messageContentBitmap,
topLeft[0].toFloat(), topLeft[0].toFloat(),
topLeft[1].toFloat(), topLeft[1].toFloat(),
visibleMessageView.messageContentView.width, messageContentView.width,
message.isOutgoing, message.isOutgoing,
visibleMessageView.messageContentView messageContentView
) )
reactionDelegate.show(this, message, selectedConversationModel, viewModel.blindedPublicKey) reactionDelegate.show(this, message, selectedConversationModel, viewModel.blindedPublicKey)
} }
@ -2056,7 +2061,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
} }
override fun selectMessages(messages: Set<MessageRecord>) { override fun selectMessages(messages: Set<MessageRecord>) {
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 // Note: The messages in the provided set may be a single message, or multiple if there are a

@ -5,6 +5,7 @@ import android.database.Cursor
import android.util.SparseArray import android.util.SparseArray
import android.util.SparseBooleanArray import android.util.SparseBooleanArray
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import androidx.core.util.getOrDefault import androidx.core.util.getOrDefault
@ -35,7 +36,7 @@ class ConversationAdapter(
private val isReversed: Boolean, private val isReversed: Boolean,
private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit,
private val onItemSwipeToReply: (MessageRecord, Int) -> 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 onDeselect: (MessageRecord, Int) -> Unit,
private val onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit, private val onAttachmentNeedsDownload: (DatabaseAttachment) -> Unit,
private val glide: RequestManager, private val glide: RequestManager,
@ -156,12 +157,18 @@ class ConversationAdapter(
} else { } else {
visibleMessageView.onPress = null visibleMessageView.onPress = null
visibleMessageView.onSwipeToReply = 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 -> { is ControlMessageViewHolder -> {
viewHolder.view.bind(message, messageBefore) viewHolder.view.bind(
message = message,
previous = messageBefore,
longPress = { onItemLongPress(message, viewHolder.adapterPosition, viewHolder.view) }
)
} }
} }
} }

@ -21,6 +21,7 @@ import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.view.doOnLayout import androidx.core.view.doOnLayout
import androidx.core.view.isVisible
import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat
import com.squareup.phrase.Phrase import com.squareup.phrase.Phrase
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -527,18 +528,25 @@ class ConversationReactionOverlay : FrameLayout {
?: return emptyList() ?: return emptyList()
val userPublicKey = getLocalNumber(context)!! val userPublicKey = getLocalNumber(context)!!
// Select message // 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 // Reply
val canWrite = openGroup == null || openGroup.canWrite 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) items += ActionItem(R.attr.menu_reply_icon, R.string.reply, { handleActionItemClicked(Action.REPLY) }, R.string.AccessibilityId_reply)
} }
// Copy message text // 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) }) items += ActionItem(R.attr.menu_copy_icon, R.string.copy, { handleActionItemClicked(Action.COPY_MESSAGE) })
} }
// Copy Account ID // 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) }) items += ActionItem(R.attr.menu_copy_icon, R.string.accountIDCopy, { handleActionItemClicked(Action.COPY_ACCOUNT_ID) })
} }
// Delete message // Delete message
@ -547,15 +555,20 @@ class ConversationReactionOverlay : FrameLayout {
R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger)) R.string.AccessibilityId_deleteMessage, message.subtitle, ThemeUtil.getThemedColor(context, R.attr.danger))
} }
// Ban user // 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) }) items += ActionItem(R.attr.menu_block_icon, R.string.banUser, { handleActionItemClicked(Action.BAN_USER) })
} }
// Ban and delete all // 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) }) items += ActionItem(R.attr.menu_trash_icon, R.string.banDeleteAll, { handleActionItemClicked(Action.BAN_AND_DELETE_ALL) })
} }
// Message detail // 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 // Resend
if (message.isFailed) { if (message.isFailed) {
items += ActionItem(R.attr.menu_reply_icon, R.string.resend, { handleActionItemClicked(Action.RESEND) }) 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) }) items += ActionItem(R.attr.menu_reply_icon, R.string.resync, { handleActionItemClicked(Action.RESYNC) })
} }
// Save media.. // 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. // ..but only provide the save option if the there is a media attachment which has finished downloading.
val mmsMessage = message as MediaMmsMessageRecord val mmsMessage = message as MediaMmsMessageRecord
if (mmsMessage.containsMediaSlide() && !mmsMessage.isMediaPending) { 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 return items
} }

@ -245,12 +245,11 @@ class ConversationViewModel(
val conversationType = conversation.getType() val conversationType = conversation.getType()
// hashes are required if wanting to delete messages from the 'storage server' - they are not required for communities // 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 { // also we can only delete deleted messages (marked as deleted) locally
lokiMessageDb.getMessageServerHash( val canDeleteForEveryone = messages.all{ !it.isDeleted } && (
it.id, conversationType == MessageType.COMMUNITY ||
it.isMms messages.all { lokiMessageDb.getMessageServerHash(it.id, it.isMms) != null
) != null })
}
// There are three types of dialogs for deletion: // There are three types of dialogs for deletion:
// 1- Delete on device only OR all devices - Used for Note to self // 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 // for non admins, users interacting with someone else's message, or control messages
else -> { else -> {
//todo DELETION this should also happen for ControlMessages
_dialogsState.update { _dialogsState.update {
it.copy(deleteDeviceOnly = messages) it.copy(deleteDeviceOnly = messages)
} }
@ -300,23 +298,29 @@ class ConversationViewModel(
} }
/** /**
* This will mark the messages as deleted, locally only. * This delete the message locally only.
* Attachments and other related data will be removed from the db, * Attachments and other related data will be removed from the db.
* but the messages themselves won't be removed from the db. * If the messages were already marked as deleted they will be removed fully from the db,
* Instead they will appear as a special type of message * otherwise they will appear as a special type of message
* that says something like "This message was deleted" * that says something like "This message was deleted"
*/ */
fun markAsDeletedLocally(messages: Set<MessageRecord>) { fun deletedLocally(messages: Set<MessageRecord>) {
// make sure to stop audio messages, if any // make sure to stop audio messages, if any
messages.filterIsInstance<MmsMessageRecord>() messages.filterIsInstance<MmsMessageRecord>()
.mapNotNull { it.slideDeck.audioSlide } .mapNotNull { it.slideDeck.audioSlide }
.forEach(::stopMessageAudio) .forEach(::stopMessageAudio)
// if the message was already marked as deleted, remove it from the db instead
repository.markAsDeletedLocally( if(messages.all { it.isDeleted }){
messages = messages, // Remove the message locally (leave nothing behind)
displayedMessage = application.getString(R.string.deleteMessageDeletedLocally) 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 // show confirmation toast
Toast.makeText( Toast.makeText(
@ -730,7 +734,7 @@ class ConversationViewModel(
it.copy(deleteDeviceOnly = null) it.copy(deleteDeviceOnly = null)
} }
markAsDeletedLocally(command.messages) deletedLocally(command.messages)
} }
is Commands.MarkAsDeletedForEveryone -> { is Commands.MarkAsDeletedForEveryone -> {
markAsDeletedForEveryone(command.data) markAsDeletedForEveryone(command.data)

@ -58,7 +58,7 @@ class ControlMessageView : LinearLayout {
layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT) 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.dateBreakTextView.showDateBreak(message, previous)
binding.iconImageView.isGone = true binding.iconImageView.isGone = true
binding.expirationTimerView.isGone = true binding.expirationTimerView.isGone = true
@ -199,6 +199,17 @@ class ControlMessageView : LinearLayout {
binding.textView.isGone = message.isCallLog binding.textView.isGone = message.isCallLog
binding.callView.isVisible = 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(){ fun showInfo(){

@ -485,10 +485,13 @@ class VisibleMessageView : FrameLayout {
// region Interaction // region Interaction
@SuppressLint("ClickableViewAccessibility") @SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean { 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) { when (event.action) {
MotionEvent.ACTION_DOWN -> onDown(event) 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_CANCEL -> onCancel(event)
MotionEvent.ACTION_UP -> onUp(event) MotionEvent.ACTION_UP -> onUp(event)
} }

Loading…
Cancel
Save