Add pending and failed states for admin delete.

This commit is contained in:
Michelle Tang
2026-03-03 11:14:11 -05:00
committed by Greyson Parrelli
parent 3af8b6050c
commit 74d9e3248b
19 changed files with 247 additions and 46 deletions

View File

@@ -291,7 +291,11 @@ public class ConversationItemFooter extends ConstraintLayout {
dateView.setText(null);
} else if (messageRecord.isFailed()) {
int errorMsg;
if (messageRecord.hasFailedWithNetworkFailures()) {
if (messageRecord.isFailedAdminDelete() && messageRecord.isIdentityMismatchFailure()) {
errorMsg = R.string.ConversationItem_error_partially_not_deleted;
} else if (messageRecord.isFailedAdminDelete()) {
errorMsg = R.string.ConversationItem_error_delete_failed;
} else if (messageRecord.hasFailedWithNetworkFailures()) {
errorMsg = R.string.ConversationItem_error_network_not_delivered;
} else if (messageRecord.getToRecipient().isPushGroup() && messageRecord.isIdentityMismatchFailure()) {
errorMsg = R.string.ConversationItem_error_partially_not_delivered;
@@ -397,7 +401,7 @@ public class ConversationItemFooter extends ConstraintLayout {
}
if (onlyShowSendingStatus) {
if (messageRecord.isOutgoing() && messageRecord.isPending()) {
if (messageRecord.isPending()) {
deliveryStatusView.setPending();
} else {
deliveryStatusView.setNone();

View File

@@ -183,7 +183,9 @@ public final class SafetyNumberChangeRepository {
}
}
if (messageRecord.isOutgoing()) {
if (messageRecord.isFailedAdminDelete()) {
processAdminDeletedMessageRecord(changedRecipients, messageRecord);
} else if (messageRecord.isOutgoing()) {
processOutgoingMessageRecord(changedRecipients, messageRecord);
}
@@ -223,6 +225,24 @@ public final class SafetyNumberChangeRepository {
}
}
@WorkerThread
private void processAdminDeletedMessageRecord(@NonNull List<ChangedRecipient> changedRecipients, @NonNull MessageRecord messageRecord) {
Log.d(TAG, "processAdminDeletedMessageRecord");
Set<RecipientId> resendIds = new HashSet<>();
for (ChangedRecipient changedRecipient : changedRecipients) {
RecipientId id = changedRecipient.getRecipient().getId();
IdentityKey identityKey = changedRecipient.getIdentityRecord().getIdentityKey();
SignalDatabase.messages().removeMismatchedIdentity(messageRecord.getId(), id, identityKey);
resendIds.add(id);
}
if (Util.hasItems(resendIds) ) {
MessageSender.resendAdminDelete(messageRecord, resendIds.stream().collect(Collectors.toList()));
}
}
static final class SafetyNumberChangeState {
private final List<ChangedRecipient> changedRecipients;

View File

@@ -94,6 +94,25 @@ object ConversationDialogs {
.show()
}
fun displayDeletionFailedDialog(context: Context, messageRecord: MessageRecord, canRetry: Boolean) {
if (canRetry) {
MaterialAlertDialogBuilder(context)
.setMessage(R.string.conversation_activity__message_failed_to_delete_retry)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.conversation_activity__send) { _, _ ->
SignalExecutors.BOUNDED.execute {
MessageSender.resendAdminDelete(messageRecord, messageRecord.networkFailures.map { it.recipientId })
}
}
.show()
} else {
MaterialAlertDialogBuilder(context)
.setMessage(R.string.conversation_activity__message_failed_to_delete)
.setPositiveButton(android.R.string.ok, null)
.show()
}
}
@JvmStatic
fun displayDeleteDialog(context: Context, recipient: Recipient, onDelete: () -> Unit) {
MaterialAlertDialogBuilder(context)

View File

@@ -3344,9 +3344,18 @@ class ConversationFragment :
override fun onMessageWithErrorClicked(messageRecord: MessageRecord) {
val recipientId = viewModel.recipientSnapshot?.id ?: return
if (messageRecord.isIdentityMismatchFailure) {
if (messageRecord.isFailedAdminDelete) {
val canRetry = MessageConstraintsUtil.isValidAdminDeleteSend(message = messageRecord, currentTime = System.currentTimeMillis(), isAdmin = conversationGroupViewModel.isAdmin(), isResend = true)
if (messageRecord.isIdentityMismatchFailure && canRetry) {
SafetyNumberBottomSheet
.forIncomingMessageRecord(messageRecord, viewModel.recipientSnapshot!!)
.show(childFragmentManager)
} else {
ConversationDialogs.displayDeletionFailedDialog(requireContext(), messageRecord, canRetry)
}
} else if (messageRecord.isIdentityMismatchFailure) {
SafetyNumberBottomSheet
.forMessageRecord(requireContext(), messageRecord)
.forOutgoingMessageRecord(requireContext(), messageRecord)
.show(childFragmentManager)
} else if (messageRecord.hasFailedWithNetworkFailures()) {
ConversationDialogs.displayMessageCouldNotBeSentDialog(requireContext(), messageRecord)

View File

@@ -106,6 +106,7 @@ import org.thoughtcrime.securesms.database.model.StoryResult
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.StoryType.Companion.fromCode
import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.database.model.databaseprotos.AdminDeleteStatus
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context
import org.thoughtcrime.securesms.database.model.databaseprotos.GV2UpdateDescription
@@ -3788,6 +3789,45 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
}
}
/**
* Sets admin delete status to pending
*/
fun markAsPendingAdminDelete(messageId: Long) {
val messageExtras = MessageExtras(adminDeleteStatus = AdminDeleteStatus(AdminDeleteStatus.Status.PENDING))
writableDatabase
.update(TABLE_NAME)
.values(MESSAGE_EXTRAS to messageExtras.encode())
.where("$ID = ?", messageId)
.run()
AppDependencies.databaseObserver.notifyMessageUpdateObservers(MessageId(messageId))
}
/**
* Sets admin delete status to failed
*/
fun markAsFailedAdminDelete(messageId: Long) {
val messageExtras = MessageExtras(adminDeleteStatus = AdminDeleteStatus(AdminDeleteStatus.Status.FAILED))
writableDatabase
.update(TABLE_NAME)
.values(MESSAGE_EXTRAS to messageExtras.encode())
.where("$ID = ?", messageId)
.run()
AppDependencies.databaseObserver.notifyMessageUpdateObservers(MessageId(messageId))
}
/**
* Sets admin delete status to complete.
*/
fun markAsSentAdminDelete(messageId: Long) {
val messageExtras = MessageExtras(adminDeleteStatus = AdminDeleteStatus(AdminDeleteStatus.Status.DONE))
writableDatabase
.update(TABLE_NAME)
.values(MESSAGE_EXTRAS to messageExtras.encode())
.where("$ID = ?", messageId)
.run()
AppDependencies.databaseObserver.notifyMessageUpdateObservers(MessageId(messageId))
}
/**
* When a message gets deleted, clear the pinned record and remove any references
*/

View File

@@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.components.transfercontrols.TransferControlVie
import org.thoughtcrime.securesms.database.MessageTypes;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
import org.thoughtcrime.securesms.database.model.databaseprotos.AdminDeleteStatus;
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
@@ -173,6 +174,15 @@ public abstract class MessageRecord extends DisplayRecord {
return MessageTypes.isLegacyType(type);
}
@Override
public boolean isFailed() {
return super.isFailed() || isFailedAdminDelete();
}
@Override
public boolean isPending() {
return super.isPending() || isPendingAdminDelete();
}
@Override
@WorkerThread
@@ -789,6 +799,18 @@ public abstract class MessageRecord extends DisplayRecord {
return deletedBy;
}
public boolean isPendingAdminDelete() {
return messageExtras != null &&
messageExtras.adminDeleteStatus != null &&
messageExtras.adminDeleteStatus.status == AdminDeleteStatus.Status.PENDING;
}
public boolean isFailedAdminDelete() {
return messageExtras != null &&
messageExtras.adminDeleteStatus != null &&
messageExtras.adminDeleteStatus.status == AdminDeleteStatus.Status.FAILED;
}
public boolean isInMemoryMessageRecord() {
return false;
}

View File

@@ -4,6 +4,7 @@ import org.signal.core.models.ServiceId
import org.signal.core.util.logging.Log
import org.signal.core.util.logging.Log.tag
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.documents.NetworkFailure
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
@@ -37,15 +38,19 @@ class AdminDeleteSendJob private constructor(
private val TAG = tag(AdminDeleteSendJob::class.java)
@JvmStatic
fun create(messageId: Long): AdminDeleteSendJob? {
val message = SignalDatabase.messages.getMessageRecord(messageId)
fun create(messageId: Long, filterRecipients: List<RecipientId>): AdminDeleteSendJob? {
val message = SignalDatabase.messages.getMessageRecordOrNull(messageId)
if (message == null) {
return null
}
val conversationRecipient = SignalDatabase.threads.getRecipientForThreadId(message.threadId)
if (conversationRecipient == null) {
return null
}
val recipientIds = conversationRecipient.participantIds.map { it.toLong() }.toMutableList()
val recipientIds = filterRecipients.ifEmpty { conversationRecipient.participantIds }.map { it.toLong() }.toMutableList()
return AdminDeleteSendJob(
messageId = messageId,
@@ -81,7 +86,11 @@ class AdminDeleteSendJob private constructor(
return Result.failure()
}
val recipients = recipientIds.map { Recipient.resolved(RecipientId.from(it)) }.toMutableList()
val existingNetworkFailures = message.networkFailures.toMutableSet()
val existingIdentityMismatches = message.identityKeyMismatches.toMutableSet()
val targets = (recipientIds + existingIdentityMismatches.map { it.recipientId.toLong() } + existingNetworkFailures.map { it.recipientId.toLong() }).toSet()
val recipients = targets.map { Recipient.resolved(RecipientId.from(it)) }.toMutableList()
val targetSentTimestamp = message.dateSent
val targetAuthor = message.fromRecipient.requireServiceId()
@@ -103,9 +112,22 @@ class AdminDeleteSendJob private constructor(
}
val eligible = RecipientUtil.getEligibleForSending(recipients.filter { it.hasServiceId })
val skippedRecipients = recipients - eligible
val ineligibleRecipients = recipients - eligible
val sendResult = deliver(conversationRecipient, eligible, targetAuthor, targetSentTimestamp)
val completedIds = sendResult.completed.map { it.id }.toSet()
existingNetworkFailures.removeAll { completedIds.contains(it.recipientId) }
existingIdentityMismatches.removeAll { completedIds.contains(it.recipientId) }
val ineligibleIds = (ineligibleRecipients.map { it.id } + sendResult.unregistered).toSet()
existingNetworkFailures.removeAll { ineligibleIds.contains(it.recipientId) }
existingIdentityMismatches.removeAll { ineligibleIds.contains(it.recipientId) }
existingIdentityMismatches.addAll(sendResult.identityMismatch)
SignalDatabase.messages.setNetworkFailures(messageId, existingNetworkFailures)
SignalDatabase.messages.setMismatchedIdentities(messageId, existingIdentityMismatches)
for (completion in sendResult.completed) {
recipientIds.remove(completion.id.toLong())
}
@@ -114,22 +136,35 @@ class AdminDeleteSendJob private constructor(
SignalDatabase.recipients.markUnregistered(unregistered)
}
for (recipient in skippedRecipients) {
for (recipient in ineligibleRecipients) {
recipientIds.remove(recipient.id.toLong())
}
Log.i(TAG, "Completed now: ${sendResult.completed.size} Skipped: ${skippedRecipients.size + sendResult.skipped.size} Remaining: ${recipientIds.size}")
Log.i(TAG, "Completed now: ${sendResult.completed.size} Skipped: ${ineligibleRecipients.size + sendResult.skipped.size} Remaining: ${recipientIds.size}")
if (recipientIds.isEmpty()) {
if (existingNetworkFailures.isEmpty() && existingIdentityMismatches.isEmpty() && recipientIds.isEmpty()) {
SignalDatabase.messages.markAsSentAdminDelete(messageId)
return Result.success()
} else if (existingIdentityMismatches.isNotEmpty()) {
Log.w(TAG, "Failing because there were ${existingIdentityMismatches.size} identity mismatches.")
return Result.failure()
} else {
Log.w(TAG, "Still need to send to ${recipients.size} recipients. Retrying.")
Log.w(TAG, "Still need to send to ${recipientIds.size} recipients. Retrying.")
return Result.retry(defaultBackoff())
}
}
override fun onFailure() {
Log.w(TAG, "Failed to send admin delete to all recipients! ${initialRecipientCount - recipientIds.size} / $initialRecipientCount")
Log.w(TAG, "Failed to send admin delete to all recipients! ${initialRecipientCount - recipientIds.size} / $initialRecipientCount. Marking remaining non-identity mismatched failures as network failure.")
val message = SignalDatabase.messages.getMessageRecordOrNull(messageId)
if (message == null) {
Log.w(TAG, "Message no longer exists, ignoring.")
} else {
val existingIdentityMismatches = message.identityKeyMismatches.map { it.recipientId.toLong() }
recipientIds.removeAll { existingIdentityMismatches.contains(it) }
SignalDatabase.messages.setNetworkFailures(messageId, recipientIds.map { NetworkFailure(RecipientId.from(it)) }.toSet())
SignalDatabase.messages.markAsFailedAdminDelete(messageId)
}
}
private fun deliver(

View File

@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.jobs;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.RecipientAccessList;
@@ -20,16 +21,18 @@ public final class GroupSendJobHelper {
}
public static @NonNull SendResult getCompletedSends(@NonNull List<Recipient> possibleRecipients, @NonNull Collection<SendMessageResult> results) {
RecipientAccessList accessList = new RecipientAccessList(possibleRecipients);
List<Recipient> completions = new ArrayList<>(results.size());
List<RecipientId> skipped = new ArrayList<>();
List<RecipientId> unregistered = new ArrayList<>();
RecipientAccessList accessList = new RecipientAccessList(possibleRecipients);
List<Recipient> completions = new ArrayList<>(results.size());
List<RecipientId> skipped = new ArrayList<>();
List<RecipientId> unregistered = new ArrayList<>();
List<IdentityKeyMismatch> identityMismatch = new ArrayList<>();
for (SendMessageResult sendMessageResult : results) {
Recipient recipient = accessList.requireByAddress(sendMessageResult.getAddress());
if (sendMessageResult.getIdentityFailure() != null) {
Log.w(TAG, "Identity failure for " + recipient.getId());
identityMismatch.add(new IdentityKeyMismatch(recipient.getId(), sendMessageResult.getIdentityFailure().getIdentityKey()));
}
if (sendMessageResult.isUnregisteredFailure()) {
@@ -63,7 +66,7 @@ public final class GroupSendJobHelper {
}
}
return new SendResult(completions, skipped, unregistered);
return new SendResult(completions, skipped, unregistered, identityMismatch);
}
public static class SendResult {
@@ -76,10 +79,14 @@ public final class GroupSendJobHelper {
/** Recipients that were discovered to be unregistered. Important: items in this list can overlap with other lists in the result. */
public final List<RecipientId> unregistered;
public SendResult(@NonNull List<Recipient> completed, @NonNull List<RecipientId> skipped, @NonNull List<RecipientId> unregistered) {
this.completed = completed;
this.skipped = skipped;
this.unregistered = unregistered;
/** Recipients that were not sent to due to an identity failure. Important: items in this list overlap with other lists in the result. */
public final List<IdentityKeyMismatch> identityMismatch;
public SendResult(@NonNull List<Recipient> completed, @NonNull List<RecipientId> skipped, @NonNull List<RecipientId> unregistered, List<IdentityKeyMismatch> identityMismatch) {
this.completed = completed;
this.skipped = skipped;
this.unregistered = unregistered;
this.identityMismatch = identityMismatch;
}
}
}

View File

@@ -45,7 +45,7 @@ import org.thoughtcrime.securesms.polls.PollOption
import org.thoughtcrime.securesms.polls.PollRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet.forMessageRecord
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet.forOutgoingMessageRecord
import org.thoughtcrime.securesms.stickers.StickerLocator
import org.thoughtcrime.securesms.util.Material3OnScrollHelper
import org.thoughtcrime.securesms.util.fragments.requireListener
@@ -153,7 +153,7 @@ class MessageDetailsFragment : Fragment(), MessageDetailsAdapter.Callbacks {
}
override fun onErrorClicked(messageRecord: MessageRecord) {
forMessageRecord(requireContext(), messageRecord)
forOutgoingMessageRecord(requireContext(), messageRecord)
.show(childFragmentManager)
}

View File

@@ -10,6 +10,7 @@ import org.thoughtcrime.securesms.database.model.IdentityRecord
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.whispersystems.signalservice.api.util.Preconditions
@@ -73,14 +74,14 @@ object SafetyNumberBottomSheet {
}
/**
* Create a factory to generate a sheet for the given message record. This will try
* Create a factory to generate a sheet for an outgoing message record. This will try
* to resend the message automatically when the user confirms.
*
* @param context Not held on to, so any context is fine.
* @param messageRecord The message record containing failed identities.
*/
@JvmStatic
fun forMessageRecord(context: Context, messageRecord: MessageRecord): Factory {
fun forOutgoingMessageRecord(context: Context, messageRecord: MessageRecord): Factory {
val args = SafetyNumberBottomSheetArgs(
untrustedRecipients = messageRecord.identityKeyMismatches.map { it.recipientId },
destinations = getDestinationFromRecord(messageRecord),
@@ -90,6 +91,21 @@ object SafetyNumberBottomSheet {
return SheetFactory(args)
}
/**
* Create a factory to generate a sheet for an incoming message record. This will try
* to resend the message automatically when the user confirms.
*/
@JvmStatic
fun forIncomingMessageRecord(messageRecord: MessageRecord, conversationRecipient: Recipient): Factory {
val args = SafetyNumberBottomSheetArgs(
untrustedRecipients = messageRecord.identityKeyMismatches.map { it.recipientId },
destinations = listOf(ContactSearchKey.RecipientSearchKey(conversationRecipient.id, false)),
messageId = MessageId(messageRecord.id)
)
return SheetFactory(args)
}
/**
* Create a factory to generate a sheet for the given identity records and destinations.
*

View File

@@ -519,9 +519,9 @@ public class MessageSender {
}
public static void sendAdminDelete(long messageId) {
// TODO(michelle): Update with failure states
SignalDatabase.messages().markAsDeleteBySelf(messageId);
AdminDeleteSendJob job = AdminDeleteSendJob.create(messageId);
SignalDatabase.messages().markAsPendingAdminDelete(messageId);
AdminDeleteSendJob job = AdminDeleteSendJob.create(messageId, Collections.emptyList());
if (job != null) {
AppDependencies.getJobManager().add(job);
} else {
@@ -529,6 +529,16 @@ public class MessageSender {
}
}
public static void resendAdminDelete(MessageRecord message, List<RecipientId> filteredRecipients) {
SignalDatabase.messages().markAsPendingAdminDelete(message.getId());
AdminDeleteSendJob job = AdminDeleteSendJob.create(message.getId(), filteredRecipients);
if (job != null) {
AppDependencies.getJobManager().add(job);
} else {
Log.w(TAG, "[resendAdminDelete] Could not resend the admin delete job.");
}
}
public static void resendGroupMessage(@NonNull Context context, @NonNull MessageRecord messageRecord, @NonNull Set<RecipientId> filterRecipientIds) {
if (!messageRecord.isMms()) throw new AssertionError("Not Group");
sendGroupPush(context, messageRecord.getToRecipient(), messageRecord.getId(), filterRecipientIds, Collections.emptyList());

View File

@@ -288,7 +288,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
} else if (model.data.primaryStory.messageRecord.isOutgoing && model.data.primaryStory.messageRecord.isFailed) {
if (model.data.primaryStory.messageRecord.isIdentityMismatchFailure) {
SafetyNumberBottomSheet
.forMessageRecord(requireContext(), model.data.primaryStory.messageRecord)
.forOutgoingMessageRecord(requireContext(), model.data.primaryStory.messageRecord)
.show(childFragmentManager)
} else {
StoryDialogs.resendStory(requireContext()) {

View File

@@ -118,7 +118,7 @@ class MyStoriesFragment : DSLSettingsFragment(
if (it.distributionStory.messageRecord.isOutgoing && it.distributionStory.messageRecord.isFailed) {
if (it.distributionStory.messageRecord.isIdentityMismatchFailure) {
SafetyNumberBottomSheet
.forMessageRecord(requireContext(), it.distributionStory.messageRecord)
.forOutgoingMessageRecord(requireContext(), it.distributionStory.messageRecord)
.show(childFragmentManager)
} else {
StoryDialogs.resendStory(requireContext()) {

View File

@@ -833,7 +833,7 @@ class StoryViewerPageFragment :
viewModel.setIsDisplayingPartialSendDialog(true)
if (storyPost.conversationMessage.messageRecord.isIdentityMismatchFailure) {
SafetyNumberBottomSheet
.forMessageRecord(requireContext(), storyPost.conversationMessage.messageRecord)
.forOutgoingMessageRecord(requireContext(), storyPost.conversationMessage.messageRecord)
.show(childFragmentManager)
} else {
StoryDialogs.resendStory(requireContext(), { viewModel.setIsDisplayingPartialSendDialog(false) }) {

View File

@@ -315,7 +315,7 @@ class StoryGroupReplyFragment :
if (messageRecord.isIdentityMismatchFailure) {
SafetyNumberBottomSheet
.forMessageRecord(requireContext(), messageRecord)
.forOutgoingMessageRecord(requireContext(), messageRecord)
.show(childFragmentManager)
} else if (messageRecord.hasFailedWithNetworkFailures()) {
MaterialAlertDialogBuilder(requireContext())

View File

@@ -55,7 +55,7 @@ object MessageConstraintsUtil {
@JvmStatic
fun isValidAdminDeleteSend(targetMessages: Collection<MessageRecord>, currentTime: Long, isAdmin: Boolean): Boolean {
return targetMessages.all { isValidAdminDeleteSend(it, currentTime, isAdmin) }
return targetMessages.all { isValidAdminDeleteSend(message = it, currentTime = currentTime, isAdmin = isAdmin, isResend = false) }
}
@JvmStatic
@@ -110,13 +110,13 @@ object MessageConstraintsUtil {
(currentTime - message.dateSent < SEND_THRESHOLD || message.toRecipient.isSelf)
}
private fun isValidAdminDeleteSend(message: MessageRecord, currentTime: Long, isAdmin: Boolean): Boolean {
fun isValidAdminDeleteSend(message: MessageRecord, currentTime: Long, isAdmin: Boolean, isResend: Boolean): Boolean {
return RemoteConfig.sendAdminDelete &&
isAdmin &&
!message.isUpdate &&
message.isPush &&
(!message.toRecipient.isGroup || message.toRecipient.isActiveGroup) &&
!message.isRemoteDelete &&
(!message.isRemoteDelete || isResend) &&
!message.hasGiftBadge() &&
!message.isPaymentNotification &&
!message.isPaymentTombstone &&

View File

@@ -536,6 +536,7 @@ message MessageExtras {
PaymentTombstone paymentTombstone = 4;
PollTerminate pollTerminate = 5;
PinnedMessage pinnedMessage = 6;
AdminDeleteStatus adminDeleteStatus = 7;
}
}
@@ -550,6 +551,15 @@ message PaymentTombstone {
CryptoValue fee = 3;
}
message AdminDeleteStatus {
enum Status {
PENDING = 0;
DONE = 1;
FAILED = 2;
}
Status status = 1;
}
message PollTerminate {
string question = 1;
uint64 messageId = 2;

View File

@@ -285,21 +285,20 @@
</org.thoughtcrime.securesms.conversation.ConversationItemBodyBubble>
<org.thoughtcrime.securesms.components.AlertView
android:id="@+id/indicators_parent"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_toStartOf="@id/quoted_indicator"
android:gravity="center_vertical"
android:visibility="gone" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/body_bubble"
android:layout_alignEnd="@id/body_bubble"
android:layout_alignBottom="@id/body_bubble"
android:layout_marginEnd="-42dp">
android:layout_marginEnd="-30dp" >
<org.thoughtcrime.securesms.components.AlertView
android:id="@+id/indicators_parent"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="bottom"
android:visibility="gone" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/quoted_indicator"
@@ -309,6 +308,7 @@
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/signal_colorSurfaceVariant"
android:padding="6dp"
android:layout_marginEnd="-12dp"
android:tint="@color/signal_colorOnSurfaceVariant"
app:srcCompat="@drawable/ic_replies_outline_20" />
@@ -320,6 +320,7 @@
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/signal_colorSurfaceVariant"
android:padding="6dp"
android:layout_marginEnd="-12dp"
android:tint="@color/signal_colorOnSurfaceVariant"
android:visibility="gone"
app:srcCompat="@drawable/symbol_chat_arrow_24" />

View File

@@ -435,7 +435,11 @@
<!-- ConversationItem -->
<string name="ConversationItem_error_not_sent_tap_for_details">Not sent, tap for details</string>
<string name="ConversationItem_error_partially_not_delivered">Partially sent, tap for details</string>
<!-- Warning footer when an admin delete has not been sent to everyone -->
<string name="ConversationItem_error_partially_not_deleted">Partially deleted, tap for details</string>
<string name="ConversationItem_error_network_not_delivered">Send failed</string>
<!-- Warning footer when an admin delete has failed to send -->
<string name="ConversationItem_error_delete_failed">Delete failed, tap for details</string>
<string name="ConversationItem_group_action_left">%1$s has left the group.</string>
<string name="ConversationItem_send_paused">Send paused</string>
<string name="ConversationItem_click_to_approve_unencrypted_sms_dialog_title">Fallback to unencrypted SMS?</string>
@@ -3592,6 +3596,10 @@
<!-- Accessibility content description describing how a user can lock voice note recording. -->
<string name="conversation_activity__quick_attachment_drawer_lock_record_description">Lock recording of audio attachment</string>
<string name="conversation_activity__message_could_not_be_sent">Message could not be sent. Check your connection and try again.</string>
<!-- Dialog body when a message failed to delete and retry is possible. -->
<string name="conversation_activity__message_failed_to_delete_retry">Message failed to delete. Check your connection and try again.</string>
<!-- Dialog body when a message failed to delete. -->
<string name="conversation_activity__message_failed_to_delete">Message failed to delete.</string>
<!-- conversation_input_panel -->
<string name="conversation_input_panel__slide_to_cancel">Slide to cancel</string>