Add support for admin delete.

This commit is contained in:
Michelle Tang
2026-02-20 14:44:34 -05:00
committed by Cody Henthorne
parent 1968438ebb
commit 071fbfd916
45 changed files with 648 additions and 132 deletions

View File

@@ -32,7 +32,8 @@ object DeleteDialog {
messageRecords: Set<MessageRecord>,
title: CharSequence? = null,
message: CharSequence = context.resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageRecords.size, messageRecords.size),
forceRemoteDelete: Boolean = false
forceRemoteDelete: Boolean = false,
isAdmin: Boolean = false
): Single<Pair<Boolean, Boolean>> = Single.create { emitter ->
val builder = MaterialAlertDialogBuilder(context)
@@ -43,7 +44,7 @@ object DeleteDialog {
val isNoteToSelfDelete = isNoteToSelfDelete(messageRecords)
if (forceRemoteDelete) {
builder.setPositiveButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> deleteForEveryone(messageRecords, emitter) }
builder.setPositiveButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> deleteForEveryone(messageRecords = messageRecords, isAdminDelete = false, emitter = emitter) }
} else {
val positiveButton = if (isNoteToSelfDelete) {
R.string.ConversationFragment_delete
@@ -58,7 +59,9 @@ object DeleteDialog {
}
if (MessageConstraintsUtil.isValidRemoteDeleteSend(messageRecords, System.currentTimeMillis()) && !isNoteToSelfDelete) {
builder.setNeutralButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> handleDeleteForEveryone(context, messageRecords, emitter) }
builder.setNeutralButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> handleDeleteForEveryone(context = context, messageRecords = messageRecords, isAdminDelete = false, emitter = emitter) }
} else if (MessageConstraintsUtil.isValidAdminDeleteSend(messageRecords, System.currentTimeMillis(), isAdmin)) {
builder.setNeutralButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> handleAdminDeleteForEveryone(context, messageRecords, emitter) }
}
}
@@ -71,15 +74,27 @@ object DeleteDialog {
return messageRecords.all { messageRecord: MessageRecord -> messageRecord.isOutgoing && messageRecord.toRecipient.isSelf }
}
private fun handleDeleteForEveryone(context: Context, messageRecords: Set<MessageRecord>, emitter: SingleEmitter<Pair<Boolean, Boolean>>) {
private fun handleAdminDeleteForEveryone(context: Context, messageRecords: Set<MessageRecord>, emitter: SingleEmitter<Pair<Boolean, Boolean>>) {
MaterialAlertDialogBuilder(context)
.setTitle("${context.getString(R.string.ConversationFragment_delete_for_everyone_title)} - INTERNAL ONLY")
.setMessage(R.string.ConversationFragment_delete_for_everyone_body)
.setPositiveButton(R.string.ConversationFragment_delete_for_everyone) { _, _ ->
handleDeleteForEveryone(context = context, messageRecords = messageRecords, isAdminDelete = true, emitter = emitter)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> emitter.onSuccess(Pair(false, false)) }
.setOnCancelListener { emitter.onSuccess(Pair(false, false)) }
.show()
}
private fun handleDeleteForEveryone(context: Context, messageRecords: Set<MessageRecord>, isAdminDelete: Boolean, emitter: SingleEmitter<Pair<Boolean, Boolean>>) {
if (SignalStore.uiHints.hasConfirmedDeleteForEveryoneOnce()) {
deleteForEveryone(messageRecords, emitter)
deleteForEveryone(messageRecords, isAdminDelete, emitter)
} else {
MaterialAlertDialogBuilder(context)
.setMessage(R.string.ConversationFragment_this_message_will_be_deleted_for_everyone_in_the_conversation)
.setPositiveButton(R.string.ConversationFragment_delete_for_everyone) { _, _ ->
SignalStore.uiHints.markHasConfirmedDeleteForEveryoneOnce()
deleteForEveryone(messageRecords, emitter)
deleteForEveryone(messageRecords, isAdminDelete, emitter)
}
.setNegativeButton(android.R.string.cancel) { _, _ -> emitter.onSuccess(Pair(false, false)) }
.setOnCancelListener { emitter.onSuccess(Pair(false, false)) }
@@ -87,10 +102,14 @@ object DeleteDialog {
}
}
private fun deleteForEveryone(messageRecords: Set<MessageRecord>, emitter: SingleEmitter<Pair<Boolean, Boolean>>) {
private fun deleteForEveryone(messageRecords: Set<MessageRecord>, isAdminDelete: Boolean, emitter: SingleEmitter<Pair<Boolean, Boolean>>) {
SignalExecutors.BOUNDED.execute {
messageRecords.forEach { message ->
MessageSender.sendRemoteDelete(message.id)
if (isAdminDelete) {
MessageSender.sendAdminDelete(message.id)
} else {
MessageSender.sendRemoteDelete(message.id)
}
}
emitter.onSuccess(Pair(true, false))

View File

@@ -1,10 +1,11 @@
package org.thoughtcrime.securesms.util
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.GroupRecord
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.milliseconds
/**
@@ -12,8 +13,10 @@ import kotlin.time.Duration.Companion.milliseconds
* have strict time limits.
*/
object MessageConstraintsUtil {
private val RECEIVE_THRESHOLD = TimeUnit.DAYS.toMillis(2)
private val SEND_THRESHOLD = TimeUnit.DAYS.toMillis(1)
private val SEND_THRESHOLD = RemoteConfig.regularDeleteThreshold.milliseconds.inWholeMilliseconds
private val RECEIVE_THRESHOLD = SEND_THRESHOLD + 1.days.inWholeMilliseconds
private val ADMIN_SEND_THRESHOLD = RemoteConfig.adminDeleteThreshold.milliseconds.inWholeMilliseconds
private val ADMIN_RECEIVE_THRESHOLD = ADMIN_SEND_THRESHOLD + 1.days.inWholeMilliseconds
const val MAX_EDIT_COUNT = 10
@@ -31,6 +34,14 @@ object MessageConstraintsUtil {
((deleteServerTimestamp - messageTimestamp < RECEIVE_THRESHOLD) || (selfIsDeleteSender && targetMessage.isOutgoing))
}
@JvmStatic
fun isValidAdminDeleteReceive(targetMessage: MessageRecord, deleteSender: Recipient, deleteServerTimestamp: Long, groupRecord: GroupRecord): Boolean {
val isValidSender = groupRecord.isAdmin(deleteSender)
val messageTimestamp = targetMessage.dateSent
return isValidSender && (deleteServerTimestamp - messageTimestamp < ADMIN_RECEIVE_THRESHOLD)
}
@JvmStatic
fun isValidEditMessageReceive(targetMessage: MessageRecord, editSender: Recipient, editServerTimestamp: Long): Boolean {
return isValidRemoteDeleteReceive(targetMessage, editSender.id, editServerTimestamp)
@@ -42,6 +53,11 @@ object MessageConstraintsUtil {
return targetMessages.all { isValidRemoteDeleteSend(it, currentTime) }
}
@JvmStatic
fun isValidAdminDeleteSend(targetMessages: Collection<MessageRecord>, currentTime: Long, isAdmin: Boolean): Boolean {
return targetMessages.all { isValidAdminDeleteSend(it, currentTime, isAdmin) }
}
@JvmStatic
fun isWithinMaxEdits(targetMessage: MessageRecord): Boolean {
return targetMessage.revisionNumber < MAX_EDIT_COUNT
@@ -94,6 +110,19 @@ object MessageConstraintsUtil {
(currentTime - message.dateSent < SEND_THRESHOLD || message.toRecipient.isSelf)
}
private fun isValidAdminDeleteSend(message: MessageRecord, currentTime: Long, isAdmin: Boolean): Boolean {
return RemoteConfig.sendAdminDelete &&
isAdmin &&
!message.isUpdate &&
message.isPush &&
(!message.toRecipient.isGroup || message.toRecipient.isActiveGroup) &&
!message.isRemoteDelete &&
!message.hasGiftBadge() &&
!message.isPaymentNotification &&
!message.isPaymentTombstone &&
(currentTime - message.dateSent < ADMIN_SEND_THRESHOLD)
}
private fun isSelf(recipientId: RecipientId): Boolean {
return Recipient.isSelfSet && Recipient.self().id == recipientId
}

View File

@@ -1257,5 +1257,48 @@ object RemoteConfig {
hotSwappable = true
)
/**
* Whether or not to receive admin delete messages.
*/
@JvmStatic
@get:JvmName("receiveAdminDelete")
val receiveAdminDelete: Boolean by remoteBoolean(
key = "android.receiveAdminDelete",
defaultValue = false,
hotSwappable = true
)
/**
* Whether or not to send admin delete messages.
*/
@JvmStatic
@get:JvmName("sendAdminDelete")
val sendAdminDelete: Boolean by remoteBoolean(
key = "android.sendAdminDelete",
defaultValue = false,
hotSwappable = true
)
/**
* Maximum time that passes where a message can still be regularly deleted
*/
@JvmStatic
@get:JvmName("regularDeleteThreshold")
val regularDeleteThreshold: Long by remoteLong(
key = "android.regularDeleteThreshold",
defaultValue = 1.days.inWholeMilliseconds,
hotSwappable = true
)
/**
* Maximum time that passes where a message can still be deleted by an admin
*/
@JvmStatic
@get:JvmName("adminDeleteThreshold")
val adminDeleteThreshold: Long by remoteLong(
key = "android.adminDeleteThreshold",
defaultValue = 1.days.inWholeMilliseconds,
hotSwappable = true
)
// endregion
}