Add message editing feature.

This commit is contained in:
Clark
2023-04-14 16:29:26 -04:00
committed by Cody Henthorne
parent 4f06a0d27c
commit 07f6baf7c1
73 changed files with 2051 additions and 304 deletions

View File

@@ -45,7 +45,7 @@ object DeleteDialog {
DeleteProgressDialogAsyncTask(context, messageRecords, emitter::onSuccess).executeOnExecutor(SignalExecutors.BOUNDED)
}
if (RemoteDeleteUtil.isValidSend(messageRecords, System.currentTimeMillis())) {
if (MessageConstraintsUtil.isValidRemoteDeleteSend(messageRecords, System.currentTimeMillis())) {
builder.setNeutralButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> handleDeleteForEveryone(context, messageRecords, emitter) }
}
}

View File

@@ -109,6 +109,8 @@ public final class FeatureFlags {
private static final String CALLS_TAB = "android.calls.tab";
private static final String TEXT_FORMATTING_SPOILER_SEND = "android.textFormatting.spoilerSend";
private static final String AD_HOC_CALLING = "android.calling.ad.hoc";
private static final String EDIT_MESSAGE_RECEIVE = "android.editMessage.receive";
private static final String EDIT_MESSAGE_SEND = "android.editMessage.send";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -167,7 +169,9 @@ public final class FeatureFlags {
TEXT_FORMATTING,
ANY_ADDRESS_PORTS_KILL_SWITCH,
CALLS_TAB,
TEXT_FORMATTING_SPOILER_SEND
TEXT_FORMATTING_SPOILER_SEND,
EDIT_MESSAGE_RECEIVE,
EDIT_MESSAGE_SEND
);
@VisibleForTesting
@@ -232,7 +236,9 @@ public final class FeatureFlags {
PAYMENTS_REQUEST_ACTIVATE_FLOW,
CDS_HARD_LIMIT,
TEXT_FORMATTING,
TEXT_FORMATTING_SPOILER_SEND
TEXT_FORMATTING_SPOILER_SEND,
EDIT_MESSAGE_RECEIVE,
EDIT_MESSAGE_SEND
);
/**
@@ -598,6 +604,14 @@ public final class FeatureFlags {
return getBoolean(ANY_ADDRESS_PORTS_KILL_SWITCH, false);
}
public static boolean editMessageReceiving() {
return getBoolean(EDIT_MESSAGE_RECEIVE, false);
}
public static boolean editMessageSending() {
return getBoolean(EDIT_MESSAGE_SEND, false);
}
/**
* Whether or not the calls tab is enabled
*/

View File

@@ -0,0 +1,83 @@
package org.thoughtcrime.securesms.util
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.hours
/**
* Helpers for determining if a message send/receive is valid for those that
* have strict time limits.
*/
object MessageConstraintsUtil {
private val RECEIVE_THRESHOLD = TimeUnit.DAYS.toMillis(1)
private val SEND_THRESHOLD = TimeUnit.HOURS.toMillis(3)
private val MAX_EDIT_COUNT = 10
@JvmStatic
fun isValidRemoteDeleteReceive(targetMessage: MessageRecord, deleteSenderId: RecipientId, deleteServerTimestamp: Long): Boolean {
val selfIsDeleteSender = isSelf(deleteSenderId)
val isValidIncomingOutgoing = selfIsDeleteSender && targetMessage.isOutgoing || !selfIsDeleteSender && !targetMessage.isOutgoing
val isValidSender = targetMessage.fromRecipient.id == deleteSenderId || selfIsDeleteSender && targetMessage.isOutgoing
val messageTimestamp = if (selfIsDeleteSender && targetMessage.isOutgoing) targetMessage.dateSent else targetMessage.serverTimestamp
return isValidIncomingOutgoing &&
isValidSender &&
((deleteServerTimestamp - messageTimestamp < RECEIVE_THRESHOLD) || (selfIsDeleteSender && targetMessage.isOutgoing))
}
@JvmStatic
fun isValidEditMessageReceive(targetMessage: MessageRecord, editSender: Recipient, editServerTimestamp: Long): Boolean {
return isValidRemoteDeleteReceive(targetMessage, editSender.id, editServerTimestamp)
}
@JvmStatic
fun isValidRemoteDeleteSend(targetMessages: Collection<MessageRecord>, currentTime: Long): Boolean {
// TODO [greyson] [remote-delete] Update with server timestamp when available for outgoing messages
return targetMessages.all { isValidRemoteDeleteSend(it, currentTime) }
}
@JvmStatic
fun getEditMessageThresholdHours(): Int {
return SEND_THRESHOLD.hours.inWholeHours.toInt()
}
/**
* Check if at the current time a target message can be edited
*/
@JvmStatic
fun isValidEditMessageSend(targetMessage: MessageRecord, currentTime: Long): Boolean {
return isValidRemoteDeleteSend(targetMessage, currentTime) &&
targetMessage.revisionNumber < 10 &&
!targetMessage.isViewOnceMessage() &&
!targetMessage.hasAudio() &&
!targetMessage.hasSharedContact()
}
/**
* Check regardless of timing, whether a target message can be edited
*/
@JvmStatic
fun isValidEditMessageSend(targetMessage: MessageRecord): Boolean {
return isValidEditMessageSend(targetMessage, targetMessage.dateSent)
}
private fun isValidRemoteDeleteSend(message: MessageRecord, currentTime: Long): Boolean {
return !message.isUpdate &&
message.isOutgoing &&
message.isPush &&
(!message.toRecipient.isGroup || message.toRecipient.isActiveGroup) &&
!message.isRemoteDelete &&
!message.hasGiftBadge() &&
!message.isPaymentNotification &&
(currentTime - message.dateSent < SEND_THRESHOLD || message.toRecipient.isSelf)
}
private fun isSelf(recipientId: RecipientId): Boolean {
return Recipient.isSelfSet() && Recipient.self().id == recipientId
}
}

View File

@@ -151,3 +151,7 @@ fun MessageRecord.isScheduled(): Boolean {
fun MessageRecord.getRecordQuoteType(): QuoteModel.Type {
return if (hasGiftBadge()) QuoteModel.Type.GIFT_BADGE else QuoteModel.Type.NORMAL
}
fun MessageRecord.isEditMessage(): Boolean {
return this is MediaMmsMessageRecord && isEditMessage
}

View File

@@ -1,60 +0,0 @@
package org.thoughtcrime.securesms.util;
import androidx.annotation.NonNull;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
public final class RemoteDeleteUtil {
private static final long RECEIVE_THRESHOLD = TimeUnit.DAYS.toMillis(1);
private static final long SEND_THRESHOLD = TimeUnit.HOURS.toMillis(3);
private RemoteDeleteUtil() {}
public static boolean isValidReceive(@NonNull MessageRecord targetMessage, @NonNull Recipient deleteSender, long deleteServerTimestamp) {
return isValidReceive(targetMessage, deleteSender.getId(), deleteServerTimestamp);
}
public static boolean isValidReceive(@NonNull MessageRecord targetMessage, @NonNull RecipientId deleteSenderId, long deleteServerTimestamp) {
boolean selfIsDeleteSender = isSelf(deleteSenderId);
boolean isValidIncomingOutgoing = (selfIsDeleteSender && targetMessage.isOutgoing()) ||
(!selfIsDeleteSender && !targetMessage.isOutgoing());
boolean isValidSender = targetMessage.getFromRecipient().getId().equals(deleteSenderId) || selfIsDeleteSender && targetMessage.isOutgoing();
long messageTimestamp = selfIsDeleteSender && targetMessage.isOutgoing() ? targetMessage.getDateSent()
: targetMessage.getServerTimestamp();
return isValidIncomingOutgoing &&
isValidSender &&
(((deleteServerTimestamp - messageTimestamp) < RECEIVE_THRESHOLD) || (selfIsDeleteSender && targetMessage.isOutgoing()));
}
public static boolean isValidSend(@NonNull Collection<MessageRecord> targetMessages, long currentTime) {
// TODO [greyson] [remote-delete] Update with server timestamp when available for outgoing messages
return Stream.of(targetMessages).allMatch(message -> isValidSend(message, currentTime));
}
private static boolean isValidSend(MessageRecord message, long currentTime) {
return !message.isUpdate() &&
message.isOutgoing() &&
message.isPush() &&
(!message.getToRecipient().isGroup() || message.getToRecipient().isActiveGroup()) &&
!message.isRemoteDelete() &&
!MessageRecordUtil.hasGiftBadge(message) &&
!message.isPaymentNotification() &&
(((currentTime - message.getDateSent()) < SEND_THRESHOLD) || message.getToRecipient().isSelf());
}
private static boolean isSelf(@NonNull RecipientId recipientId) {
return Recipient.isSelfSet() && Recipient.self().getId().equals(recipientId);
}
}

View File

@@ -0,0 +1,25 @@
package org.thoughtcrime.securesms.util
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
/**
* Simplifies [ViewModel] creation by providing default implementations of [ViewModelProvider.Factory]
* and a factory producer that call through to a lambda to create the view model instance.
*
* Example use:
*
* private val viewModel: MyViewModel by viewModels(factoryProducer = ViewModelFactory.factoryProducer { MyViewModel(inputParams) })
*/
class ViewModelFactory<MODEL : ViewModel>(private val create: () -> MODEL) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return create() as T
}
companion object {
fun <MODEL : ViewModel> factoryProducer(create: () -> MODEL): () -> ViewModelProvider.Factory {
return { ViewModelFactory(create) }
}
}
}