mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-08 20:18:34 +00:00
Add pre-alpha receive support for remote delete.
This commit is contained in:
parent
456bcf3d57
commit
6ecd3b59fd
@ -110,6 +110,7 @@ import org.thoughtcrime.securesms.stickers.StickerPackPreviewActivity;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.HtmlUtil;
|
||||
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
@ -602,6 +603,14 @@ public class ConversationFragment extends Fragment
|
||||
}
|
||||
|
||||
private void handleDeleteMessages(final Set<MessageRecord> messageRecords) {
|
||||
if (FeatureFlags.remoteDelete()) {
|
||||
buildRemoteDeleteConfirmationDialog(messageRecords).show();
|
||||
} else {
|
||||
buildLegacyDeleteConfirmationDialog(messageRecords).show();
|
||||
}
|
||||
}
|
||||
|
||||
private AlertDialog.Builder buildLegacyDeleteConfirmationDialog(Set<MessageRecord> messageRecords) {
|
||||
int messagesCount = messageRecords.size();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
|
||||
@ -610,15 +619,13 @@ public class ConversationFragment extends Fragment
|
||||
builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messagesCount, messagesCount));
|
||||
builder.setCancelable(true);
|
||||
|
||||
builder.setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
new ProgressDialogAsyncTask<MessageRecord, Void, Void>(getActivity(),
|
||||
builder.setPositiveButton(R.string.delete, (dialog, which) -> {
|
||||
new ProgressDialogAsyncTask<Void, Void, Void>(getActivity(),
|
||||
R.string.ConversationFragment_deleting,
|
||||
R.string.ConversationFragment_deleting_messages)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground(MessageRecord... messageRecords) {
|
||||
protected Void doInBackground(Void... voids) {
|
||||
for (MessageRecord messageRecord : messageRecords) {
|
||||
boolean threadDeleted;
|
||||
|
||||
@ -636,14 +643,63 @@ public class ConversationFragment extends Fragment
|
||||
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, messageRecords.toArray(new MessageRecord[messageRecords.size()]));
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
});
|
||||
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
return builder;
|
||||
}
|
||||
|
||||
private AlertDialog.Builder buildRemoteDeleteConfirmationDialog(Set<MessageRecord> messageRecords) {
|
||||
Context context = requireActivity();
|
||||
int messagesCount = messageRecords.size();
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
|
||||
builder.setTitle(getActivity().getResources().getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messagesCount, messagesCount));
|
||||
builder.setCancelable(true);
|
||||
|
||||
builder.setPositiveButton(R.string.ConversationFragment_delete_for_me, (dialog, which) -> {
|
||||
new ProgressDialogAsyncTask<Void, Void, Void>(getActivity(),
|
||||
R.string.ConversationFragment_deleting,
|
||||
R.string.ConversationFragment_deleting_messages)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
for (MessageRecord messageRecord : messageRecords) {
|
||||
boolean threadDeleted;
|
||||
|
||||
if (messageRecord.isMms()) {
|
||||
threadDeleted = DatabaseFactory.getMmsDatabase(context).delete(messageRecord.getId());
|
||||
} else {
|
||||
threadDeleted = DatabaseFactory.getSmsDatabase(context).deleteMessage(messageRecord.getId());
|
||||
}
|
||||
|
||||
if (threadDeleted) {
|
||||
threadId = -1;
|
||||
listener.setThreadId(threadId);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
});
|
||||
|
||||
if (RemoteDeleteUtil.isValidSend(messageRecords, System.currentTimeMillis())) {
|
||||
builder.setNeutralButton(R.string.ConversationFragment_delete_for_everyone, (dialog, which) -> {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
for (MessageRecord message : messageRecords) {
|
||||
MessageSender.sendRemoteDelete(context, message.getId(), message.isMms());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
||||
private void handleDisplayDetails(MessageRecord message) {
|
||||
Intent intent = new Intent(getActivity(), MessageDetailsActivity.class);
|
||||
intent.putExtra(MessageDetailsActivity.MESSAGE_ID_EXTRA, message.getId());
|
||||
@ -1086,6 +1142,7 @@ public class ConversationFragment extends Fragment
|
||||
if (actionMode != null) return;
|
||||
|
||||
if (messageRecord.isSecure() &&
|
||||
!messageRecord.isRemoteDelete() &&
|
||||
!messageRecord.isUpdate() &&
|
||||
!recipient.get().isBlocked() &&
|
||||
!messageRequestViewModel.shouldShowMessageRequest() &&
|
||||
|
@ -36,6 +36,8 @@ import android.text.style.BackgroundColorSpan;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.RelativeSizeSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.text.style.URLSpan;
|
||||
import android.text.util.Linkify;
|
||||
import android.util.AttributeSet;
|
||||
@ -519,7 +521,14 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
||||
bodyText.setFocusable(false);
|
||||
bodyText.setTextSize(TypedValue.COMPLEX_UNIT_SP, TextSecurePreferences.getMessageBodyTextSize(context));
|
||||
|
||||
if (isCaptionlessMms(messageRecord)) {
|
||||
if (messageRecord.isRemoteDelete()) {
|
||||
String deletedMessage = context.getString(R.string.ConversationItem_this_message_was_deleted);
|
||||
SpannableString italics = new SpannableString(deletedMessage);
|
||||
italics.setSpan(new RelativeSizeSpan(0.9f), 0, deletedMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
italics.setSpan(new StyleSpan(android.graphics.Typeface.ITALIC), 0, deletedMessage.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
bodyText.setText(italics);
|
||||
} else if (isCaptionlessMms(messageRecord)) {
|
||||
bodyText.setVisibility(View.GONE);
|
||||
} else {
|
||||
Spannable styledText = linkifyMessageBody(messageRecord.getDisplayBody(getContext()), batchSelected.isEmpty());
|
||||
|
@ -432,6 +432,7 @@ public final class ConversationReactionOverlay extends RelativeLayout {
|
||||
toolbar.getMenu().findItem(R.id.action_copy).setVisible(menuState.shouldShowCopyAction());
|
||||
toolbar.getMenu().findItem(R.id.action_download).setVisible(menuState.shouldShowSaveAttachmentAction());
|
||||
toolbar.getMenu().findItem(R.id.action_forward).setVisible(menuState.shouldShowForwardAction());
|
||||
toolbar.getMenu().findItem(R.id.action_reply).setVisible(menuState.shouldShowReplyAction());
|
||||
}
|
||||
|
||||
private boolean handleToolbarItemClicked(@NonNull MenuItem menuItem) {
|
||||
|
@ -59,6 +59,7 @@ final class MenuState {
|
||||
boolean hasText = false;
|
||||
boolean sharedContact = false;
|
||||
boolean viewOnce = false;
|
||||
boolean remoteDelete = false;
|
||||
|
||||
for (MessageRecord messageRecord : messageRecords) {
|
||||
if (isActionMessage(messageRecord))
|
||||
@ -77,6 +78,10 @@ final class MenuState {
|
||||
if (messageRecord.isViewOnce()) {
|
||||
viewOnce = true;
|
||||
}
|
||||
|
||||
if (messageRecord.isRemoteDelete()) {
|
||||
remoteDelete = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (messageRecords.size() > 1) {
|
||||
@ -95,17 +100,18 @@ final class MenuState {
|
||||
!messageRecord.isMmsNotification() &&
|
||||
((MediaMmsMessageRecord)messageRecord).containsMediaSlide() &&
|
||||
((MediaMmsMessageRecord)messageRecord).getSlideDeck().getStickerSlide() == null)
|
||||
.shouldShowForwardAction(!actionMessage && !sharedContact && !viewOnce)
|
||||
.shouldShowForwardAction(!actionMessage && !sharedContact && !viewOnce && !remoteDelete)
|
||||
.shouldShowDetailsAction(!actionMessage)
|
||||
.shouldShowReplyAction(canReplyToMessage(actionMessage, messageRecord, shouldShowMessageRequest));
|
||||
}
|
||||
|
||||
return builder.shouldShowCopyAction(!actionMessage && hasText)
|
||||
return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText)
|
||||
.build();
|
||||
}
|
||||
|
||||
static boolean canReplyToMessage(boolean actionMessage, @NonNull MessageRecord messageRecord, boolean isDisplayingMessageRequest) {
|
||||
return !actionMessage &&
|
||||
!messageRecord.isRemoteDelete() &&
|
||||
!messageRecord.isPending() &&
|
||||
!messageRecord.isFailed() &&
|
||||
!isDisplayingMessageRequest &&
|
||||
|
@ -50,6 +50,9 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
||||
public abstract void markAsSent(long messageId, boolean secure);
|
||||
public abstract void markUnidentified(long messageId, boolean unidentified);
|
||||
|
||||
public abstract void markAsSending(long messageId);
|
||||
public abstract void markAsRemoteDelete(long messageId);
|
||||
|
||||
final int getInsecureMessagesSentForThread(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] projection = new String[]{"COUNT(*)"};
|
||||
|
@ -168,7 +168,8 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
VIEW_ONCE + " INTEGER DEFAULT 0, " +
|
||||
REACTIONS + " BLOB DEFAULT NULL, " +
|
||||
REACTIONS_UNREAD + " INTEGER DEFAULT 0, " +
|
||||
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1);";
|
||||
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " +
|
||||
REMOTE_DELETED + " INTEGER DEFAULT 0);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS mms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
|
||||
@ -193,6 +194,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID,
|
||||
EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING,
|
||||
SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
|
||||
REMOTE_DELETED,
|
||||
"json_group_array(json_object(" +
|
||||
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
|
||||
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
|
||||
@ -473,6 +475,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsSending(long messageId) {
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
updateMailboxBitmask(messageId, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE, Optional.of(threadId));
|
||||
@ -492,6 +495,29 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsRemoteDelete(long messageId) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(REMOTE_DELETED, 1);
|
||||
values.putNull(BODY);
|
||||
values.putNull(QUOTE_BODY);
|
||||
values.putNull(QUOTE_AUTHOR);
|
||||
values.putNull(QUOTE_ATTACHMENT);
|
||||
values.putNull(QUOTE_ID);
|
||||
values.putNull(LINK_PREVIEWS);
|
||||
values.putNull(SHARED_CONTACTS);
|
||||
values.putNull(REACTIONS);
|
||||
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(messageId) });
|
||||
|
||||
DatabaseFactory.getAttachmentDatabase(context).deleteAttachmentsForMessage(messageId);
|
||||
|
||||
long threadId = getThreadIdForMessage(messageId);
|
||||
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
|
||||
public void markDownloadState(long messageId, long state) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues contentValues = new ContentValues();
|
||||
@ -1506,7 +1532,8 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
message.getSharedContacts(),
|
||||
message.getLinkPreviews(),
|
||||
false,
|
||||
Collections.emptyList());
|
||||
Collections.emptyList(),
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1597,6 +1624,7 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.EXPIRE_STARTED));
|
||||
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.UNIDENTIFIED)) == 1;
|
||||
boolean isViewOnce = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.VIEW_ONCE)) == 1;
|
||||
boolean remoteDelete = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.REMOTE_DELETED)) == 1;
|
||||
List<ReactionRecord> reactions = parseReactions(cursor);
|
||||
|
||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
||||
@ -1618,7 +1646,8 @@ public class MmsDatabase extends MessagingDatabase {
|
||||
addressDeviceId, dateSent, dateReceived, dateServer, deliveryReceiptCount,
|
||||
threadId, body, slideDeck, partCount, box, mismatches,
|
||||
networkFailures, subscriptionId, expiresIn, expireStarted,
|
||||
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, reactions);
|
||||
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, reactions,
|
||||
remoteDelete);
|
||||
}
|
||||
|
||||
private List<IdentityKeyMismatch> getMismatchedIdentities(String document) {
|
||||
|
@ -24,6 +24,7 @@ public interface MmsSmsColumns {
|
||||
public static final String REACTIONS = "reactions";
|
||||
public static final String REACTIONS_UNREAD = "reactions_unread";
|
||||
public static final String REACTIONS_LAST_SEEN = "reactions_last_seen";
|
||||
public static final String REMOTE_DELETED = "remote_deleted";
|
||||
|
||||
public static class Types {
|
||||
protected static final long TOTAL_MASK = 0xFFFFFFFF;
|
||||
|
@ -28,6 +28,7 @@ import net.sqlcipher.database.SQLiteQueryBuilder;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
@ -87,7 +88,8 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsSmsColumns.READ,
|
||||
MmsSmsColumns.REACTIONS,
|
||||
MmsSmsColumns.REACTIONS_UNREAD,
|
||||
MmsSmsColumns.REACTIONS_LAST_SEEN};
|
||||
MmsSmsColumns.REACTIONS_LAST_SEEN,
|
||||
MmsSmsColumns.REMOTE_DELETED};
|
||||
|
||||
public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
@ -395,7 +397,8 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.REACTIONS,
|
||||
MmsSmsColumns.REACTIONS_UNREAD,
|
||||
MmsSmsColumns.REACTIONS_LAST_SEEN,
|
||||
MmsSmsColumns.DATE_SERVER };
|
||||
MmsSmsColumns.DATE_SERVER,
|
||||
MmsSmsColumns.REMOTE_DELETED };
|
||||
|
||||
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
|
||||
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
|
||||
@ -426,7 +429,8 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.REACTIONS,
|
||||
MmsSmsColumns.REACTIONS_UNREAD,
|
||||
MmsSmsColumns.REACTIONS_LAST_SEEN,
|
||||
MmsSmsColumns.DATE_SERVER };
|
||||
MmsSmsColumns.DATE_SERVER,
|
||||
MmsSmsColumns.REMOTE_DELETED };
|
||||
|
||||
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
|
||||
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
|
||||
@ -478,6 +482,7 @@ public class MmsSmsDatabase extends Database {
|
||||
mmsColumnsPresent.add(MmsDatabase.REACTIONS);
|
||||
mmsColumnsPresent.add(MmsDatabase.REACTIONS_UNREAD);
|
||||
mmsColumnsPresent.add(MmsDatabase.REACTIONS_LAST_SEEN);
|
||||
mmsColumnsPresent.add(MmsDatabase.REMOTE_DELETED);
|
||||
|
||||
Set<String> smsColumnsPresent = new HashSet<>();
|
||||
smsColumnsPresent.add(MmsSmsColumns.ID);
|
||||
@ -503,6 +508,7 @@ public class MmsSmsDatabase extends Database {
|
||||
smsColumnsPresent.add(SmsDatabase.REACTIONS);
|
||||
smsColumnsPresent.add(SmsDatabase.REACTIONS_UNREAD);
|
||||
smsColumnsPresent.add(SmsDatabase.REACTIONS_LAST_SEEN);
|
||||
smsColumnsPresent.add(MmsDatabase.REMOTE_DELETED);
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 4, MMS_TRANSPORT, selection, null, MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID, null);
|
||||
|
@ -48,6 +48,7 @@ import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
@ -103,7 +104,8 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
UNIDENTIFIED + " INTEGER DEFAULT 0, " +
|
||||
REACTIONS + " BLOB DEFAULT NULL, " +
|
||||
REACTIONS_UNREAD + " INTEGER DEFAULT 0, " +
|
||||
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1);";
|
||||
REACTIONS_LAST_SEEN + " INTEGER DEFAULT -1, " +
|
||||
REMOTE_DELETED + " INTEGER DEFAULT 0);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS sms_thread_id_index ON " + TABLE_NAME + " (" + THREAD_ID + ");",
|
||||
@ -124,7 +126,8 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
PROTOCOL, READ, STATUS, TYPE,
|
||||
REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, DELIVERY_RECEIPT_COUNT,
|
||||
MISMATCHED_IDENTITIES, SUBSCRIPTION_ID, EXPIRES_IN, EXPIRE_STARTED,
|
||||
NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN
|
||||
NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED, REACTIONS, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
|
||||
REMOTE_DELETED
|
||||
};
|
||||
|
||||
private final String OUTGOING_INSECURE_MESSAGE_CLAUSE = "(" + TYPE + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND NOT (" + TYPE + " & " + Types.SECURE_MESSAGE_BIT + ")";
|
||||
@ -314,6 +317,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENT_TYPE | (isSecure ? Types.PUSH_MESSAGE_BIT | Types.SECURE_MESSAGE_BIT : 0));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsSending(long id) {
|
||||
updateTypeBitmask(id, Types.BASE_TYPE_MASK, Types.BASE_SENDING_TYPE);
|
||||
}
|
||||
@ -322,6 +326,21 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
updateTypeBitmask(id, Types.TOTAL_MASK, Types.MISSED_CALL_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markAsRemoteDelete(long id) {
|
||||
SQLiteDatabase db = databaseHelper.getWritableDatabase();
|
||||
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(REMOTE_DELETED, 1);
|
||||
values.putNull(BODY);
|
||||
db.update(TABLE_NAME, values, ID_WHERE, new String[] { String.valueOf(id) });
|
||||
|
||||
long threadId = getThreadIdForMessage(id);
|
||||
|
||||
DatabaseFactory.getThreadDatabase(context).update(threadId, false);
|
||||
notifyConversationListeners(threadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markUnidentified(long id, boolean unidentified) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
@ -913,7 +932,8 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
System.currentTimeMillis(),
|
||||
0,
|
||||
false,
|
||||
Collections.emptyList());
|
||||
Collections.emptyList(),
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
@ -955,6 +975,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
long expireStarted = cursor.getLong(cursor.getColumnIndexOrThrow(SmsDatabase.EXPIRE_STARTED));
|
||||
String body = cursor.getString(cursor.getColumnIndexOrThrow(SmsDatabase.BODY));
|
||||
boolean unidentified = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.UNIDENTIFIED)) == 1;
|
||||
boolean remoteDelete = cursor.getInt(cursor.getColumnIndexOrThrow(SmsDatabase.REMOTE_DELETED)) == 1;
|
||||
List<ReactionRecord> reactions = parseReactions(cursor);
|
||||
|
||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
||||
@ -970,7 +991,7 @@ public class SmsDatabase extends MessagingDatabase {
|
||||
dateSent, dateReceived, dateServer, deliveryReceiptCount, type,
|
||||
threadId, status, mismatches, subscriptionId,
|
||||
expiresIn, expireStarted,
|
||||
readReceiptCount, unidentified, reactions);
|
||||
readReceiptCount, unidentified, reactions, remoteDelete);
|
||||
}
|
||||
|
||||
private List<IdentityKeyMismatch> getMismatches(String document) {
|
||||
|
@ -773,8 +773,10 @@ public class ThreadDatabase extends Database {
|
||||
return Extra.forMessageRequest();
|
||||
}
|
||||
|
||||
if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) {
|
||||
return Extra.forRevealable();
|
||||
if (record.isViewOnce()) {
|
||||
return Extra.forViewOnce();
|
||||
} else if (record.isRemoteDelete()) {
|
||||
return Extra.forRemoteDelete();
|
||||
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getStickerSlide() != null) {
|
||||
return Extra.forSticker();
|
||||
} else if (record.isMms() && ((MmsMessageRecord) record).getSlideDeck().getSlides().size() > 1) {
|
||||
@ -899,43 +901,50 @@ public class ThreadDatabase extends Database {
|
||||
@JsonProperty private final boolean isRevealable;
|
||||
@JsonProperty private final boolean isSticker;
|
||||
@JsonProperty private final boolean isAlbum;
|
||||
@JsonProperty private final boolean isRemoteDelete;
|
||||
@JsonProperty private final boolean isMessageRequestAccepted;
|
||||
@JsonProperty private final String groupAddedBy;
|
||||
|
||||
public Extra(@JsonProperty("isRevealable") boolean isRevealable,
|
||||
@JsonProperty("isSticker") boolean isSticker,
|
||||
@JsonProperty("isAlbum") boolean isAlbum,
|
||||
@JsonProperty("isRemoteDelete") boolean isRemoteDelete,
|
||||
@JsonProperty("isMessageRequestAccepted") boolean isMessageRequestAccepted,
|
||||
@JsonProperty("groupAddedBy") String groupAddedBy)
|
||||
{
|
||||
this.isRevealable = isRevealable;
|
||||
this.isSticker = isSticker;
|
||||
this.isAlbum = isAlbum;
|
||||
this.isRemoteDelete = isRemoteDelete;
|
||||
this.isMessageRequestAccepted = isMessageRequestAccepted;
|
||||
this.groupAddedBy = groupAddedBy;
|
||||
}
|
||||
|
||||
public static @NonNull Extra forRevealable() {
|
||||
return new Extra(true, false, false, true, null);
|
||||
public static @NonNull Extra forViewOnce() {
|
||||
return new Extra(true, false, false, false, true, null);
|
||||
}
|
||||
|
||||
public static @NonNull Extra forSticker() {
|
||||
return new Extra(false, true, false, true, null);
|
||||
return new Extra(false, true, false, false, true, null);
|
||||
}
|
||||
|
||||
public static @NonNull Extra forAlbum() {
|
||||
return new Extra(false, false, true, true, null);
|
||||
return new Extra(false, false, true, false, true, null);
|
||||
}
|
||||
|
||||
public static @NonNull Extra forRemoteDelete() {
|
||||
return new Extra(false, false, false, true, true, null);
|
||||
}
|
||||
|
||||
public static @NonNull Extra forMessageRequest() {
|
||||
return new Extra(false, false, false, false, null);
|
||||
return new Extra(false, false, false, false, false, null);
|
||||
}
|
||||
|
||||
public static @NonNull Extra forGroupMessageRequest(RecipientId recipientId) {
|
||||
return new Extra(false, false, false, false, recipientId.serialize());
|
||||
return new Extra(false, false, false, false, false, recipientId.serialize());
|
||||
}
|
||||
|
||||
public boolean isRevealable() {
|
||||
public boolean isViewOnce() {
|
||||
return isRevealable;
|
||||
}
|
||||
|
||||
@ -947,6 +956,10 @@ public class ThreadDatabase extends Database {
|
||||
return isAlbum;
|
||||
}
|
||||
|
||||
public boolean isRemoteDelete() {
|
||||
return isRemoteDelete;
|
||||
}
|
||||
|
||||
public boolean isMessageRequestAccepted() {
|
||||
return isMessageRequestAccepted;
|
||||
}
|
||||
|
@ -129,8 +129,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
private static final int ATTACHMENT_CDN_NUMBER = 57;
|
||||
private static final int JOB_INPUT_DATA = 58;
|
||||
private static final int SERVER_TIMESTAMP = 59;
|
||||
private static final int REMOTE_DELETE = 60;
|
||||
|
||||
private static final int DATABASE_VERSION = 59;
|
||||
private static final int DATABASE_VERSION = 60;
|
||||
private static final String DATABASE_NAME = "signal.db";
|
||||
|
||||
private final Context context;
|
||||
@ -882,6 +883,11 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper {
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS mms_date_server_index ON mms (date_server)");
|
||||
}
|
||||
|
||||
if (oldVersion < REMOTE_DELETE) {
|
||||
db.execSQL("ALTER TABLE sms ADD COLUMN remote_deleted INTEGER DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN remote_deleted INTEGER DEFAULT 0");
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
@ -70,12 +70,13 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
@NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews,
|
||||
boolean unidentified,
|
||||
@NonNull List<ReactionRecord> reactions)
|
||||
@NonNull List<ReactionRecord> reactions,
|
||||
boolean remoteDelete)
|
||||
{
|
||||
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
|
||||
dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
|
||||
subscriptionId, expiresIn, expireStarted, viewOnce, slideDeck,
|
||||
readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions);
|
||||
readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions, remoteDelete);
|
||||
this.partCount = partCount;
|
||||
}
|
||||
|
||||
|
@ -55,6 +55,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
private final boolean unidentified;
|
||||
private final List<ReactionRecord> reactions;
|
||||
private final long serverTimestamp;
|
||||
private final boolean remoteDelete;
|
||||
|
||||
MessageRecord(long id, String body, Recipient conversationRecipient,
|
||||
Recipient individualRecipient, int recipientDeviceId,
|
||||
@ -64,7 +65,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
List<NetworkFailure> networkFailures,
|
||||
int subscriptionId, long expiresIn, long expireStarted,
|
||||
int readReceiptCount, boolean unidentified,
|
||||
@NonNull List<ReactionRecord> reactions)
|
||||
@NonNull List<ReactionRecord> reactions, boolean remoteDelete)
|
||||
{
|
||||
super(body, conversationRecipient, dateSent, dateReceived,
|
||||
threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount);
|
||||
@ -79,6 +80,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
this.unidentified = unidentified;
|
||||
this.reactions = reactions;
|
||||
this.serverTimestamp = dateServer;
|
||||
this.remoteDelete = remoteDelete;
|
||||
}
|
||||
|
||||
public abstract boolean isMms();
|
||||
@ -259,6 +261,10 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isRemoteDelete() {
|
||||
return remoteDelete;
|
||||
}
|
||||
|
||||
public @NonNull List<ReactionRecord> getReactions() {
|
||||
return reactions;
|
||||
}
|
||||
|
@ -33,9 +33,9 @@ public abstract class MmsMessageRecord extends MessageRecord {
|
||||
@NonNull SlideDeck slideDeck, int readReceiptCount,
|
||||
@Nullable Quote quote, @NonNull List<Contact> contacts,
|
||||
@NonNull List<LinkPreview> linkPreviews, boolean unidentified,
|
||||
@NonNull List<ReactionRecord> reactions)
|
||||
@NonNull List<ReactionRecord> reactions, boolean remoteDelete)
|
||||
{
|
||||
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, dateServer, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount, unidentified, reactions);
|
||||
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent, dateReceived, dateServer, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount, unidentified, reactions, remoteDelete);
|
||||
|
||||
this.slideDeck = slideDeck;
|
||||
this.quote = quote;
|
||||
|
@ -58,7 +58,7 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
|
||||
dateSent, dateReceived, -1, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
|
||||
new LinkedList<>(), new LinkedList<>(), subscriptionId,
|
||||
0, 0, false, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false,
|
||||
Collections.emptyList());
|
||||
Collections.emptyList(), false);
|
||||
|
||||
this.contentLocation = contentLocation;
|
||||
this.messageSize = messageSize;
|
||||
|
@ -49,12 +49,12 @@ public class SmsMessageRecord extends MessageRecord {
|
||||
int status, List<IdentityKeyMismatch> mismatches,
|
||||
int subscriptionId, long expiresIn, long expireStarted,
|
||||
int readReceiptCount, boolean unidentified,
|
||||
@NonNull List<ReactionRecord> reactions)
|
||||
@NonNull List<ReactionRecord> reactions, boolean remoteDelete)
|
||||
{
|
||||
super(id, body, recipient, individualRecipient, recipientDeviceId,
|
||||
dateSent, dateReceived, dateServer, threadId, status, deliveryReceiptCount, type,
|
||||
mismatches, new LinkedList<>(), subscriptionId,
|
||||
expiresIn, expireStarted, readReceiptCount, unidentified, reactions);
|
||||
expiresIn, expireStarted, readReceiptCount, unidentified, reactions, remoteDelete);
|
||||
}
|
||||
|
||||
public long getType() {
|
||||
|
@ -127,8 +127,10 @@ public class ThreadRecord extends DisplayRecord {
|
||||
if (TextUtils.isEmpty(getBody())) {
|
||||
if (extra != null && extra.isSticker()) {
|
||||
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_sticker)));
|
||||
} else if (extra != null && extra.isRevealable()) {
|
||||
} else if (extra != null && extra.isViewOnce()) {
|
||||
return new SpannableString(emphasisAdded(getViewOnceDescription(context, contentType)));
|
||||
} else if (extra != null && extra.isRemoteDelete()) {
|
||||
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_this_message_was_deleted)));
|
||||
} else {
|
||||
return new SpannableString(emphasisAdded(context.getString(R.string.ThreadRecord_media_message)));
|
||||
}
|
||||
|
@ -86,6 +86,7 @@ public final class JobManagerFactories {
|
||||
put(RefreshOwnProfileJob.KEY, new RefreshOwnProfileJob.Factory());
|
||||
put(RefreshPreKeysJob.KEY, new RefreshPreKeysJob.Factory());
|
||||
put(RemoteConfigRefreshJob.KEY, new RemoteConfigRefreshJob.Factory());
|
||||
put(RemoteDeleteSendJob.KEY, new RemoteDeleteSendJob.Factory());
|
||||
put(RequestGroupInfoJob.KEY, new RequestGroupInfoJob.Factory());
|
||||
put(StorageAccountRestoreJob.KEY, new StorageAccountRestoreJob.Factory());
|
||||
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory());
|
||||
|
@ -79,6 +79,7 @@ import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.thoughtcrime.securesms.util.Hex;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.state.SessionStore;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
@ -115,6 +116,7 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public final class PushProcessMessageJob extends BaseJob {
|
||||
@ -278,6 +280,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||
else if (message.isGroupV1Update()) handleGroupV1Message(content, message, smsMessageId);
|
||||
else if (message.isExpirationUpdate()) handleExpirationUpdate(content, message, smsMessageId);
|
||||
else if (message.getReaction().isPresent()) handleReaction(content, message);
|
||||
else if (message.getRemoteDelete().isPresent()) handleRemoteDelete(content, message);
|
||||
else if (isMediaMessage) handleMediaMessage(content, message, smsMessageId);
|
||||
else if (message.getBody().isPresent()) handleTextMessage(content, message, smsMessageId, groupId);
|
||||
|
||||
@ -606,7 +609,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||
Recipient targetAuthor = Recipient.externalPush(context, reaction.getTargetAuthor());
|
||||
MessageRecord targetMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(reaction.getTargetSentTimestamp(), targetAuthor.getId());
|
||||
|
||||
if (targetMessage != null) {
|
||||
if (targetMessage != null && !targetMessage.isRemoteDelete()) {
|
||||
Recipient reactionAuthor = Recipient.externalPush(context, content.getSender());
|
||||
MessagingDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
|
||||
@ -618,12 +621,31 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||
db.addReaction(targetMessage.getId(), reactionRecord);
|
||||
MessageNotifier.updateNotification(context, targetMessage.getThreadId(), false);
|
||||
}
|
||||
|
||||
} else if (targetMessage != null) {
|
||||
Log.w(TAG, "[handleReaction] Found a matching message, but it's flagged as remotely deleted. timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId());
|
||||
} else {
|
||||
Log.w(TAG, "[handleReaction] Could not find matching message! timestamp: " + reaction.getTargetSentTimestamp() + " author: " + targetAuthor.getId());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRemoteDelete(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message) {
|
||||
SignalServiceDataMessage.RemoteDelete delete = message.getRemoteDelete().get();
|
||||
|
||||
Recipient sender = Recipient.externalPush(context, content.getSender());
|
||||
MessageRecord targetMessage = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(delete.getTargetSentTimestamp(), sender.getId());
|
||||
|
||||
if (targetMessage != null && RemoteDeleteUtil.isValidReceive(targetMessage, sender, content.getServerTimestamp())) {
|
||||
MessagingDatabase db = targetMessage.isMms() ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
db.markAsRemoteDelete(targetMessage.getId());
|
||||
MessageNotifier.updateNotification(context, targetMessage.getThreadId(), false);
|
||||
} else if (targetMessage == null) {
|
||||
Log.w(TAG, "[handleRemoteDelete] Could not find matching message! timestamp: " + delete.getTargetSentTimestamp() + " author: " + sender.getId());
|
||||
} else {
|
||||
Log.w(TAG, String.format(Locale.ENGLISH, "[handleRemoteDelete] Invalid remote delete! deleteTime: %d, targetTime: %d, deleteAuthor: %s, targetAuthor: %s",
|
||||
content.getServerTimestamp(), targetMessage.getServerTimestamp(), sender.getId(), targetMessage.getRecipient().getId()));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSynchronizeVerifiedMessage(@NonNull VerifiedMessage verifiedMessage) {
|
||||
IdentityUtil.processVerifiedMessage(context, verifiedMessage);
|
||||
}
|
||||
@ -751,6 +773,8 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||
handleReaction(content, message.getMessage());
|
||||
threadId = DatabaseFactory.getThreadDatabase(context).getThreadIdFor(getSyncMessageDestination(message));
|
||||
threadId = threadId != -1 ? threadId : null;
|
||||
} else if (message.getMessage().getRemoteDelete().isPresent()) {
|
||||
handleRemoteDelete(content, message.getMessage());
|
||||
} else if (message.getMessage().getAttachments().isPresent() || message.getMessage().getQuote().isPresent() || message.getMessage().getPreviews().isPresent() || message.getMessage().getSticker().isPresent() || message.getMessage().isViewOnce()) {
|
||||
threadId = handleSynchronizeSentMediaMessage(message);
|
||||
} else {
|
||||
@ -1392,7 +1416,7 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||
RecipientId author = Recipient.externalPush(context, quote.get().getAuthor()).getId();
|
||||
MessageRecord message = DatabaseFactory.getMmsSmsDatabase(context).getMessageFor(quote.get().getId(), author);
|
||||
|
||||
if (message != null) {
|
||||
if (message != null && !message.isRemoteDelete()) {
|
||||
Log.i(TAG, "Found matching message record...");
|
||||
|
||||
List<Attachment> attachments = new LinkedList<>();
|
||||
@ -1415,6 +1439,8 @@ public final class PushProcessMessageJob extends BaseJob {
|
||||
}
|
||||
|
||||
return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), false, attachments));
|
||||
} else if (message != null) {
|
||||
Log.w(TAG, "Found the target for the quote, but it's flagged as remotely deleted.");
|
||||
}
|
||||
|
||||
Log.w(TAG, "Didn't find matching message record...");
|
||||
|
@ -0,0 +1,212 @@
|
||||
package org.thoughtcrime.securesms.jobs;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroup;
|
||||
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class RemoteDeleteSendJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "RemoteDeleteSendJob";
|
||||
|
||||
private static final String TAG = Log.tag(RemoteDeleteSendJob.class);
|
||||
|
||||
private static final String KEY_MESSAGE_ID = "message_id";
|
||||
private static final String KEY_IS_MMS = "is_mms";
|
||||
private static final String KEY_RECIPIENTS = "recipients";
|
||||
private static final String KEY_INITIAL_RECIPIENT_COUNT = "initial_recipient_count";
|
||||
|
||||
private final long messageId;
|
||||
private final boolean isMms;
|
||||
private final List<RecipientId> recipients;
|
||||
private final int initialRecipientCount;
|
||||
|
||||
|
||||
@WorkerThread
|
||||
public static @NonNull RemoteDeleteSendJob create(@NonNull Context context,
|
||||
long messageId,
|
||||
boolean isMms)
|
||||
throws NoSuchMessageException
|
||||
{
|
||||
MessageRecord message = isMms ? DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId)
|
||||
: DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
|
||||
Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(message.getThreadId());
|
||||
|
||||
if (conversationRecipient == null) {
|
||||
throw new AssertionError("We have a message, but couldn't find the thread!");
|
||||
}
|
||||
|
||||
List<RecipientId> recipients = conversationRecipient.isGroup() ? Stream.of(conversationRecipient.getParticipants()).map(Recipient::getId).toList()
|
||||
: Stream.of(conversationRecipient.getId()).toList();
|
||||
|
||||
recipients.remove(Recipient.self().getId());
|
||||
|
||||
return new RemoteDeleteSendJob(messageId,
|
||||
isMms,
|
||||
recipients,
|
||||
recipients.size(),
|
||||
new Parameters.Builder()
|
||||
.setQueue(conversationRecipient.getId().toQueueKey())
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build());
|
||||
}
|
||||
|
||||
private RemoteDeleteSendJob(long messageId,
|
||||
boolean isMms,
|
||||
@NonNull List<RecipientId> recipients,
|
||||
int initialRecipientCount,
|
||||
@NonNull Parameters parameters)
|
||||
{
|
||||
super(parameters);
|
||||
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
this.recipients = recipients;
|
||||
this.initialRecipientCount = initialRecipientCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Data serialize() {
|
||||
return new Data.Builder().putLong(KEY_MESSAGE_ID, messageId)
|
||||
.putBoolean(KEY_IS_MMS, isMms)
|
||||
.putString(KEY_RECIPIENTS, RecipientId.toSerializedList(recipients))
|
||||
.putInt(KEY_INITIAL_RECIPIENT_COUNT, initialRecipientCount)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getFactoryKey() {
|
||||
return KEY;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRun() throws Exception {
|
||||
MessagingDatabase db;
|
||||
MessageRecord message;
|
||||
|
||||
if (isMms) {
|
||||
db = DatabaseFactory.getMmsDatabase(context);
|
||||
message = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
} else {
|
||||
db = DatabaseFactory.getSmsDatabase(context);
|
||||
message = DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
}
|
||||
|
||||
long targetSentTimestamp = message.getDateSent();
|
||||
Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(message.getThreadId());
|
||||
|
||||
if (conversationRecipient == null) {
|
||||
throw new AssertionError("We have a message, but couldn't find the thread!");
|
||||
}
|
||||
|
||||
if (!message.isOutgoing()) {
|
||||
throw new IllegalStateException("Cannot delete a message that isn't yours!");
|
||||
}
|
||||
|
||||
List<Recipient> destinations = Stream.of(recipients).map(Recipient::resolved).toList();
|
||||
List<Recipient> completions = deliver(conversationRecipient, destinations, targetSentTimestamp);
|
||||
|
||||
for (Recipient completion : completions) {
|
||||
recipients.remove(completion.getId());
|
||||
}
|
||||
|
||||
Log.i(TAG, "Completed now: " + completions.size() + ", Remaining: " + recipients.size());
|
||||
|
||||
if (recipients.isEmpty()) {
|
||||
db.markAsSent(messageId, true);
|
||||
} else {
|
||||
Log.w(TAG, "Still need to send to " + recipients.size() + " recipients. Retrying.");
|
||||
throw new RetryLaterException();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(@NonNull Exception e) {
|
||||
return e instanceof IOException ||
|
||||
e instanceof RetryLaterException;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure() {
|
||||
Log.w(TAG, "Failed to send the reaction to all recipients! (" + (initialRecipientCount - recipients.size() + "/" + initialRecipientCount + ")") );
|
||||
}
|
||||
|
||||
private @NonNull List<Recipient> deliver(@NonNull Recipient conversationRecipient, @NonNull List<Recipient> destinations, long targetSentTimestamp)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
SignalServiceMessageSender messageSender = ApplicationDependencies.getSignalServiceMessageSender();
|
||||
List<SignalServiceAddress> addresses = Stream.of(destinations).map(t -> RecipientUtil.toSignalServiceAddress(context, t)).toList();
|
||||
List<Optional<UnidentifiedAccessPair>> unidentifiedAccess = Stream.of(destinations).map(recipient -> UnidentifiedAccessUtil.getAccessFor(context, recipient)).toList();
|
||||
SignalServiceDataMessage.Builder dataMessage = SignalServiceDataMessage.newBuilder()
|
||||
.withTimestamp(System.currentTimeMillis())
|
||||
.withRemoteDelete(new SignalServiceDataMessage.RemoteDelete(targetSentTimestamp));
|
||||
|
||||
if (conversationRecipient.isGroup()) {
|
||||
dataMessage.asGroupMessage(new SignalServiceGroup(conversationRecipient.requireGroupId().getDecodedId()));
|
||||
}
|
||||
|
||||
List<SendMessageResult> results = messageSender.sendMessage(addresses, unidentifiedAccess, false, dataMessage.build());
|
||||
|
||||
Stream.of(results)
|
||||
.filter(r -> r.getIdentityFailure() != null)
|
||||
.map(SendMessageResult::getAddress)
|
||||
.map(a -> Recipient.externalPush(context, a))
|
||||
.forEach(r -> Log.w(TAG, "Identity failure for " + r.getId()));
|
||||
|
||||
Stream.of(results)
|
||||
.filter(SendMessageResult::isUnregisteredFailure)
|
||||
.map(SendMessageResult::getAddress)
|
||||
.map(a -> Recipient.externalPush(context, a))
|
||||
.forEach(r -> Log.w(TAG, "Unregistered failure for " + r.getId()));
|
||||
|
||||
return Stream.of(results)
|
||||
.filter(r -> r.getSuccess() != null || r.getIdentityFailure() != null || r.isUnregisteredFailure())
|
||||
.map(SendMessageResult::getAddress)
|
||||
.map(a -> Recipient.externalPush(context, a))
|
||||
.toList();
|
||||
}
|
||||
|
||||
public static class Factory implements Job.Factory<RemoteDeleteSendJob> {
|
||||
|
||||
@Override
|
||||
public @NonNull RemoteDeleteSendJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
long messageId = data.getLong(KEY_MESSAGE_ID);
|
||||
boolean isMms = data.getBoolean(KEY_IS_MMS);
|
||||
List<RecipientId> recipients = RecipientId.fromSerializedList(data.getString(KEY_RECIPIENTS));
|
||||
int initialRecipientCount = data.getInt(KEY_INITIAL_RECIPIENT_COUNT);
|
||||
|
||||
return new RemoteDeleteSendJob(messageId, isMms, recipients, initialRecipientCount, parameters);
|
||||
}
|
||||
}
|
||||
}
|
@ -527,6 +527,8 @@ public class MessageNotifier {
|
||||
slideDeck = ((MmsMessageRecord) record).getSlideDeck();
|
||||
} else if (record.isMms() && ((MmsMessageRecord) record).isViewOnce()) {
|
||||
body = SpanUtil.italic(context.getString(getViewOnceDescription((MmsMessageRecord) record)));
|
||||
} else if (record.isRemoteDelete()) {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_this_message_was_deleted));;
|
||||
} else if (record.isMms() && TextUtils.isEmpty(body) && !((MmsMessageRecord) record).getSlideDeck().getSlides().isEmpty()) {
|
||||
body = SpanUtil.italic(context.getString(R.string.MessageNotifier_media_message));
|
||||
slideDeck = ((MediaMmsMessageRecord) record).getSlideDeck();
|
||||
|
@ -56,6 +56,7 @@ import org.thoughtcrime.securesms.jobs.PushGroupSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushMediaSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.PushTextSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.ReactionSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.RemoteDeleteSendJob;
|
||||
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
@ -309,6 +310,19 @@ public class MessageSender {
|
||||
}
|
||||
}
|
||||
|
||||
public static void sendRemoteDelete(@NonNull Context context, long messageId, boolean isMms) {
|
||||
MessagingDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
db.markAsRemoteDelete(messageId);
|
||||
db.markAsSending(messageId);
|
||||
|
||||
try {
|
||||
ApplicationDependencies.getJobManager().add(RemoteDeleteSendJob.create(context, messageId, isMms));
|
||||
onMessageSent();
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "[sendNewReaction] Could not find message! Ignoring.");
|
||||
}
|
||||
}
|
||||
|
||||
public static void resendGroupMessage(Context context, MessageRecord messageRecord, RecipientId filterRecipientId) {
|
||||
if (!messageRecord.isMms()) throw new AssertionError("Not Group");
|
||||
sendGroupPush(context, messageRecord.getRecipient(), messageRecord.getId(), filterRecipientId, Collections.emptyList());
|
||||
|
@ -55,6 +55,7 @@ public final class FeatureFlags {
|
||||
private static final String PINS_MEGAPHONE_KILL_SWITCH = "android.pinsMegaphoneKillSwitch";
|
||||
private static final String PROFILE_NAMES_MEGAPHONE = "android.profileNamesMegaphone";
|
||||
private static final String ATTACHMENTS_V3 = "android.attachmentsV3";
|
||||
private static final String REMOTE_DELETE = "android.remoteDelete";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@ -68,7 +69,8 @@ public final class FeatureFlags {
|
||||
PINS_MEGAPHONE_KILL_SWITCH,
|
||||
PROFILE_NAMES_MEGAPHONE,
|
||||
MESSAGE_REQUESTS,
|
||||
ATTACHMENTS_V3
|
||||
ATTACHMENTS_V3,
|
||||
REMOTE_DELETE
|
||||
);
|
||||
|
||||
/**
|
||||
@ -218,6 +220,11 @@ public final class FeatureFlags {
|
||||
return getValue(ATTACHMENTS_V3, false);
|
||||
}
|
||||
|
||||
/** Send support for remotely deleting a message. */
|
||||
public static boolean remoteDelete() {
|
||||
return getValue(REMOTE_DELETE, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Boolean> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
@ -0,0 +1,37 @@
|
||||
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 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.MINUTES.toMillis(30);
|
||||
|
||||
private RemoteDeleteUtil() {}
|
||||
|
||||
public static boolean isValidReceive(@NonNull MessageRecord targetMessage, @NonNull Recipient deleteSender, long deleteServerTimestamp) {
|
||||
boolean isValidSender = (deleteSender.isLocalNumber() && targetMessage.isOutgoing()) ||
|
||||
(!deleteSender.isLocalNumber() && !targetMessage.isOutgoing());
|
||||
|
||||
return isValidSender &&
|
||||
targetMessage.getIndividualRecipient().equals(deleteSender) &&
|
||||
(deleteServerTimestamp - targetMessage.getServerTimestamp()) < RECEIVE_THRESHOLD;
|
||||
}
|
||||
|
||||
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 -> message.isOutgoing() &&
|
||||
!message.isRemoteDelete() &&
|
||||
!message.isPending() &&
|
||||
(currentTime - message.getDateSent()) < SEND_THRESHOLD);
|
||||
}
|
||||
}
|
@ -186,6 +186,7 @@
|
||||
<string name="ConversationItem_read_more">  Read More</string>
|
||||
<string name="ConversationItem_download_more">  Download More</string>
|
||||
<string name="ConversationItem_pending">  Pending</string>
|
||||
<string name="ConversationItem_this_message_was_deleted">This message was deleted.</string>
|
||||
|
||||
<!-- ConversationActivity -->
|
||||
<string name="ConversationActivity_reset_secure_session_question">Reset secure session?</string>
|
||||
@ -298,6 +299,8 @@
|
||||
<string name="ConversationFragment_sms">SMS</string>
|
||||
<string name="ConversationFragment_deleting">Deleting</string>
|
||||
<string name="ConversationFragment_deleting_messages">Deleting messages…</string>
|
||||
<string name="ConversationFragment_delete_for_me">Delete for me</string>
|
||||
<string name="ConversationFragment_delete_for_everyone">Delete for everyone</string>
|
||||
<string name="ConversationFragment_quoted_message_not_found">Original message not found</string>
|
||||
<string name="ConversationFragment_quoted_message_no_longer_available">Original message no longer available</string>
|
||||
<string name="ConversationFragment_failed_to_open_message">Failed to open message</string>
|
||||
@ -959,7 +962,7 @@
|
||||
<string name="SmsMessageRecord_secure_session_reset_s">%s reset the secure session.</string>
|
||||
<string name="SmsMessageRecord_duplicate_message">Duplicate message.</string>
|
||||
<string name="SmsMessageRecord_this_message_could_not_be_processed_because_it_was_sent_from_a_newer_version">This message could not be processed because it was sent from a newer version of Signal. You can ask your contact to send this message again after you update.</string>
|
||||
<string name="SmsMessageRecord_error_handling_incoming_message">Error handling incoming message</string>
|
||||
<string name="SmsMessageRecord_error_handling_incoming_message">Error handling incoming message.</string>
|
||||
|
||||
<!-- StickerManagementActivity -->
|
||||
<string name="StickerManagementActivity_stickers">Stickers</string>
|
||||
@ -1006,6 +1009,7 @@
|
||||
<string name="ThreadRecord_view_once_photo">View-once photo</string>
|
||||
<string name="ThreadRecord_view_once_video">View-once video</string>
|
||||
<string name="ThreadRecord_view_once_media">View-once media</string>
|
||||
<string name="ThreadRecord_this_message_was_deleted">This message was deleted.</string>
|
||||
<string name="ThreadRecord_s_is_on_signal">%s is on Signal!</string>
|
||||
<string name="ThreadRecord_disappearing_messages_disabled">Disappearing messages disabled</string>
|
||||
<string name="ThreadRecord_disappearing_message_time_updated_to_s">Disappearing message time set to %s</string>
|
||||
@ -1131,6 +1135,7 @@
|
||||
<string name="MessageNotifier_reacted_s_to_your_view_once_photo">Reacted %1$s to your view-once photo.</string>
|
||||
<string name="MessageNotifier_reacted_s_to_your_view_once_video">Reacted %1$s to your view-once video.</string>
|
||||
<string name="MessageNotifier_reacted_s_to_your_sticker">Reacted %1$s to your sticker.</string>
|
||||
<string name="MessageNotifier_this_message_was_deleted">This message was deleted.</string>
|
||||
|
||||
<!-- Notification Channels -->
|
||||
<string name="NotificationChannel_messages">Default</string>
|
||||
|
@ -639,6 +639,13 @@ public class SignalServiceMessageSender {
|
||||
builder.setRequiredProtocolVersion(Math.max(DataMessage.ProtocolVersion.REACTIONS_VALUE, builder.getRequiredProtocolVersion()));
|
||||
}
|
||||
|
||||
if (message.getRemoteDelete().isPresent()) {
|
||||
DataMessage.Delete delete = DataMessage.Delete.newBuilder()
|
||||
.setTargetSentTimestamp(message.getRemoteDelete().get().getTargetSentTimestamp())
|
||||
.build();
|
||||
builder.setDelete(delete);
|
||||
}
|
||||
|
||||
builder.setTimestamp(message.getTimestamp());
|
||||
|
||||
return container.setDataMessage(builder).build().toByteArray();
|
||||
|
@ -293,6 +293,7 @@ public final class SignalServiceContent {
|
||||
List<SignalServiceDataMessage.Preview> previews = createPreviews(content);
|
||||
SignalServiceDataMessage.Sticker sticker = createSticker(content);
|
||||
SignalServiceDataMessage.Reaction reaction = createReaction(content);
|
||||
SignalServiceDataMessage.RemoteDelete remoteDelete = createRemoteDelete(content);
|
||||
|
||||
if (content.getRequiredProtocolVersion() > SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT_VALUE) {
|
||||
throw new UnsupportedDataMessageProtocolVersionException(SignalServiceProtos.DataMessage.ProtocolVersion.CURRENT_VALUE,
|
||||
@ -326,7 +327,8 @@ public final class SignalServiceContent {
|
||||
previews,
|
||||
sticker,
|
||||
content.getIsViewOnce(),
|
||||
reaction);
|
||||
reaction,
|
||||
remoteDelete);
|
||||
}
|
||||
|
||||
private static SignalServiceSyncMessage createSynchronizeMessage(SignalServiceMetadata metadata,
|
||||
@ -660,6 +662,16 @@ public final class SignalServiceContent {
|
||||
reaction.getTargetSentTimestamp());
|
||||
}
|
||||
|
||||
private static SignalServiceDataMessage.RemoteDelete createRemoteDelete(SignalServiceProtos.DataMessage content) {
|
||||
if (!content.hasDelete() || !content.getDelete().hasTargetSentTimestamp()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
SignalServiceProtos.DataMessage.Delete delete = content.getDelete();
|
||||
|
||||
return new SignalServiceDataMessage.RemoteDelete(delete.getTargetSentTimestamp());
|
||||
}
|
||||
|
||||
private static List<SharedContact> createSharedContacts(SignalServiceProtos.DataMessage content) throws ProtocolInvalidMessageException {
|
||||
if (content.getContactCount() <= 0) return null;
|
||||
|
||||
|
@ -35,6 +35,7 @@ public class SignalServiceDataMessage {
|
||||
private final Optional<Sticker> sticker;
|
||||
private final boolean viewOnce;
|
||||
private final Optional<Reaction> reaction;
|
||||
private final Optional<RemoteDelete> remoteDelete;
|
||||
|
||||
/**
|
||||
* Construct a SignalServiceDataMessage.
|
||||
@ -53,7 +54,7 @@ public class SignalServiceDataMessage {
|
||||
String body, boolean endSession, int expiresInSeconds,
|
||||
boolean expirationUpdate, byte[] profileKey, boolean profileKeyUpdate,
|
||||
Quote quote, List<SharedContact> sharedContacts, List<Preview> previews,
|
||||
Sticker sticker, boolean viewOnce, Reaction reaction)
|
||||
Sticker sticker, boolean viewOnce, Reaction reaction, RemoteDelete remoteDelete)
|
||||
{
|
||||
try {
|
||||
this.group = SignalServiceGroupContext.createOptional(group, groupV2);
|
||||
@ -72,6 +73,7 @@ public class SignalServiceDataMessage {
|
||||
this.sticker = Optional.fromNullable(sticker);
|
||||
this.viewOnce = viewOnce;
|
||||
this.reaction = Optional.fromNullable(reaction);
|
||||
this.remoteDelete = Optional.fromNullable(remoteDelete);
|
||||
|
||||
if (attachments != null && !attachments.isEmpty()) {
|
||||
this.attachments = Optional.of(attachments);
|
||||
@ -174,6 +176,10 @@ public class SignalServiceDataMessage {
|
||||
return reaction;
|
||||
}
|
||||
|
||||
public Optional<RemoteDelete> getRemoteDelete() {
|
||||
return remoteDelete;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private List<SignalServiceAttachment> attachments = new LinkedList<>();
|
||||
@ -193,6 +199,7 @@ public class SignalServiceDataMessage {
|
||||
private Sticker sticker;
|
||||
private boolean viewOnce;
|
||||
private Reaction reaction;
|
||||
private RemoteDelete remoteDelete;
|
||||
|
||||
private Builder() {}
|
||||
|
||||
@ -300,12 +307,17 @@ public class SignalServiceDataMessage {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder withRemoteDelete(RemoteDelete remoteDelete) {
|
||||
this.remoteDelete = remoteDelete;
|
||||
return this;
|
||||
}
|
||||
|
||||
public SignalServiceDataMessage build() {
|
||||
if (timestamp == 0) timestamp = System.currentTimeMillis();
|
||||
return new SignalServiceDataMessage(timestamp, group, groupV2, attachments, body, endSession,
|
||||
expiresInSeconds, expirationUpdate, profileKey,
|
||||
profileKeyUpdate, quote, sharedContacts, previews,
|
||||
sticker, viewOnce, reaction);
|
||||
sticker, viewOnce, reaction, remoteDelete);
|
||||
}
|
||||
}
|
||||
|
||||
@ -446,4 +458,16 @@ public class SignalServiceDataMessage {
|
||||
return targetSentTimestamp;
|
||||
}
|
||||
}
|
||||
|
||||
public static class RemoteDelete {
|
||||
private final long targetSentTimestamp;
|
||||
|
||||
public RemoteDelete(long targetSentTimestamp) {
|
||||
this.targetSentTimestamp = targetSentTimestamp;
|
||||
}
|
||||
|
||||
public long getTargetSentTimestamp() {
|
||||
return targetSentTimestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -184,6 +184,10 @@ message DataMessage {
|
||||
optional uint64 targetSentTimestamp = 5;
|
||||
}
|
||||
|
||||
message Delete {
|
||||
optional uint64 targetSentTimestamp = 1;
|
||||
}
|
||||
|
||||
enum ProtocolVersion {
|
||||
option allow_alias = true;
|
||||
|
||||
@ -211,6 +215,7 @@ message DataMessage {
|
||||
optional uint32 requiredProtocolVersion = 12;
|
||||
optional bool isViewOnce = 14;
|
||||
optional Reaction reaction = 16;
|
||||
optional Delete delete = 17;
|
||||
}
|
||||
|
||||
message NullMessage {
|
||||
|
Loading…
x
Reference in New Issue
Block a user