diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java index 25187d7475..bd14933f3d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java index 0af4d7ee93..ed45418d37 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java @@ -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 changedRecipients, @NonNull MessageRecord messageRecord) { + Log.d(TAG, "processAdminDeletedMessageRecord"); + Set 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 changedRecipients; diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationDialogs.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationDialogs.kt index 64daf77bf6..2b21d023c0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationDialogs.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationDialogs.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index aad652b320..5354fd7d0a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index 43824700b1..c069f49366 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -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 */ diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 97dd119ed1..dfe8645505 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AdminDeleteSendJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AdminDeleteSendJob.kt index 5f1e880d68..df34b7a4d3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/AdminDeleteSendJob.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AdminDeleteSendJob.kt @@ -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): 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( diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupSendJobHelper.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupSendJobHelper.java index 61247f6211..8c51a56c86 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupSendJobHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/GroupSendJobHelper.java @@ -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 possibleRecipients, @NonNull Collection results) { - RecipientAccessList accessList = new RecipientAccessList(possibleRecipients); - List completions = new ArrayList<>(results.size()); - List skipped = new ArrayList<>(); - List unregistered = new ArrayList<>(); + RecipientAccessList accessList = new RecipientAccessList(possibleRecipients); + List completions = new ArrayList<>(results.size()); + List skipped = new ArrayList<>(); + List unregistered = new ArrayList<>(); + List 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 unregistered; - public SendResult(@NonNull List completed, @NonNull List skipped, @NonNull List 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 identityMismatch; + + public SendResult(@NonNull List completed, @NonNull List skipped, @NonNull List unregistered, List identityMismatch) { + this.completed = completed; + this.skipped = skipped; + this.unregistered = unregistered; + this.identityMismatch = identityMismatch; } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.kt index f4147e1215..3a40deee3b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.kt @@ -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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/safety/SafetyNumberBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/safety/SafetyNumberBottomSheet.kt index 1ce41bc749..a898c26824 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/safety/SafetyNumberBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/safety/SafetyNumberBottomSheet.kt @@ -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. * diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java index 29f16855d4..81489e639e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java @@ -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 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 filterRecipientIds) { if (!messageRecord.isMms()) throw new AssertionError("Not Group"); sendGroupPush(context, messageRecord.getToRecipient(), messageRecord.getId(), filterRecipientIds, Collections.emptyList()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt index 72cf08c099..b44103d4da 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt @@ -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()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt index e2a6b51005..ca6b6a0abd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesFragment.kt @@ -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()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt index 0bcd7c3084..3c4d7ffa1d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt @@ -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) }) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt index e3c0606c81..b904f015b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/group/StoryGroupReplyFragment.kt @@ -315,7 +315,7 @@ class StoryGroupReplyFragment : if (messageRecord.isIdentityMismatchFailure) { SafetyNumberBottomSheet - .forMessageRecord(requireContext(), messageRecord) + .forOutgoingMessageRecord(requireContext(), messageRecord) .show(childFragmentManager) } else if (messageRecord.hasFailedWithNetworkFailures()) { MaterialAlertDialogBuilder(requireContext()) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MessageConstraintsUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/MessageConstraintsUtil.kt index dbcc42fc76..44d28d1721 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MessageConstraintsUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MessageConstraintsUtil.kt @@ -55,7 +55,7 @@ object MessageConstraintsUtil { @JvmStatic fun isValidAdminDeleteSend(targetMessages: Collection, 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 && diff --git a/app/src/main/protowire/Database.proto b/app/src/main/protowire/Database.proto index c5f864d4bb..b06363a779 100644 --- a/app/src/main/protowire/Database.proto +++ b/app/src/main/protowire/Database.proto @@ -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; diff --git a/app/src/main/res/layout/conversation_item_received_multimedia.xml b/app/src/main/res/layout/conversation_item_received_multimedia.xml index e2549a68b2..224bb80954 100644 --- a/app/src/main/res/layout/conversation_item_received_multimedia.xml +++ b/app/src/main/res/layout/conversation_item_received_multimedia.xml @@ -285,21 +285,20 @@ - - + android:layout_marginEnd="-30dp" > + + @@ -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" /> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 86c6fb3169..a94a12697a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -435,7 +435,11 @@ Not sent, tap for details Partially sent, tap for details + + Partially deleted, tap for details Send failed + + Delete failed, tap for details %1$s has left the group. Send paused Fallback to unencrypted SMS? @@ -3592,6 +3596,10 @@ Lock recording of audio attachment Message could not be sent. Check your connection and try again. + + Message failed to delete. Check your connection and try again. + + Message failed to delete. Slide to cancel