mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 08:09:12 +01:00
Add message editing feature.
This commit is contained in:
@@ -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) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user