From 6a87495a6d01b6f98e777410445bcac1286e00db Mon Sep 17 00:00:00 2001 From: Clark Date: Fri, 4 Aug 2023 12:35:36 -0400 Subject: [PATCH] Update contact hiding to spec. --- .../ContactsManagementRepository.kt | 4 +- .../conversation/v2/ConversationFragment.kt | 2 +- .../database/DistributionListTables.kt | 4 ++ .../securesms/database/MessageTable.kt | 59 +++++++++++++++++++ .../securesms/database/RecipientTable.kt | 16 ++--- .../database/model/RecipientRecord.kt | 2 +- .../MessageRequestRepository.java | 11 +++- .../messagerequests/MessageRequestState.java | 3 + .../messages/DataMessageProcessor.kt | 5 ++ .../securesms/recipients/Recipient.java | 42 +++++++++++-- .../recipients/RecipientDetails.java | 7 +-- .../securesms/recipients/RecipientUtil.java | 18 +++++- .../securesms/storage/StorageSyncModels.java | 2 +- .../database/RecipientDatabaseTestUtils.kt | 2 +- 14 files changed, 151 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/management/ContactsManagementRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/management/ContactsManagementRepository.kt index 813f69b4de..e110b2dece 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/management/ContactsManagementRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/management/ContactsManagementRepository.kt @@ -34,10 +34,10 @@ class ContactsManagementRepository(context: Context) { } val rotateProfileKey = !recipient.hasGroupsInCommon() - SignalDatabase.recipients.markHidden(recipient.id, rotateProfileKey) + SignalDatabase.recipients.markHidden(recipient.id, rotateProfileKey, false) if (rotateProfileKey) { ApplicationDependencies.getJobManager().add(RotateProfileKeyJob()) } - } + }.subscribeOn(Schedulers.io()) } } 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 6705b83704..f4cad66e83 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 @@ -1113,7 +1113,7 @@ class ConversationFragment : var inputDisabled = true when { inputReadyState.isClientExpired || inputReadyState.isUnauthorized -> disabledInputView.showAsExpiredOrUnauthorized(inputReadyState.isClientExpired, inputReadyState.isUnauthorized) - inputReadyState.messageRequestState != MessageRequestState.NONE -> disabledInputView.showAsMessageRequest(inputReadyState.conversationRecipient, inputReadyState.messageRequestState) + inputReadyState.messageRequestState != MessageRequestState.NONE && inputReadyState.messageRequestState != MessageRequestState.NONE_HIDDEN -> disabledInputView.showAsMessageRequest(inputReadyState.conversationRecipient, inputReadyState.messageRequestState) inputReadyState.isActiveGroup == false -> disabledInputView.showAsNoLongerAMember() inputReadyState.isRequestingMember == true -> disabledInputView.showAsRequestingMember() inputReadyState.isAnnouncementGroup == true && inputReadyState.isAdmin == false -> disabledInputView.showAsAnnouncementGroupAdminsOnly() diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DistributionListTables.kt b/app/src/main/java/org/thoughtcrime/securesms/database/DistributionListTables.kt index 5d82fa024e..68b82947e8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DistributionListTables.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DistributionListTables.kt @@ -491,6 +491,10 @@ class DistributionListTables constructor(context: Context?, databaseHelper: Sign } } + fun removeMemberFromAllLists(member: RecipientId) { + writableDatabase.delete(MembershipTable.TABLE_NAME, "${MembershipTable.RECIPIENT_ID} = ?", SqlUtil.buildArgs(member)) + } + fun removeMemberFromList(listId: DistributionListId, privacyMode: DistributionListPrivacyMode, member: RecipientId) { writableDatabase.delete(MembershipTable.TABLE_NAME, "${MembershipTable.LIST_ID} = ? AND ${MembershipTable.RECIPIENT_ID} = ? AND ${MembershipTable.PRIVACY_MODE} = ?", SqlUtil.buildArgs(listId, member, privacyMode.serialize())) } 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 e30e8e4973..8e80cad2ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -1708,6 +1708,65 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat } } + /** + * Delete all the stories received from the recipient in 1:1 stories + */ + fun deleteStoriesForRecipient(recipientId: RecipientId): Int { + return writableDatabase.withinTransaction { db -> + val threadId = threads.getThreadIdFor(recipientId) + val storesInRecipientThread = "$IS_STORY_CLAUSE AND $THREAD_ID = ?" + val sharedArgs = buildArgs(threadId) + + val deleteStoryRepliesQuery = """ + DELETE FROM $TABLE_NAME + WHERE + $PARENT_STORY_ID > 0 AND + $PARENT_STORY_ID IN ( + SELECT $ID + FROM $TABLE_NAME + WHERE $storesInRecipientThread + ) + """ + + val disassociateQuoteQuery = """ + UPDATE $TABLE_NAME + SET + $QUOTE_MISSING = 1, + $QUOTE_BODY = '' + WHERE + $PARENT_STORY_ID < 0 AND + ABS($PARENT_STORY_ID) IN ( + SELECT $ID + FROM $TABLE_NAME + WHERE $storesInRecipientThread + ) + """ + + db.execSQL(deleteStoryRepliesQuery, sharedArgs) + db.execSQL(disassociateQuoteQuery, sharedArgs) + + ApplicationDependencies.getDatabaseObserver().notifyStoryObservers(recipientId) + + val deletedStoryCount = db.select(ID) + .from(TABLE_NAME) + .where(storesInRecipientThread, sharedArgs) + .run() + .use { cursor -> + while (cursor.moveToNext()) { + deleteMessage(cursor.requireLong(ID)) + } + + cursor.count + } + + if (deletedStoryCount > 0) { + OptimizeMessageSearchIndexJob.enqueue() + } + + deletedStoryCount + } + } + private fun disassociateStoryQuotes(storyId: Long) { writableDatabase .update(TABLE_NAME) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index 269518a259..5dff8ba3c8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -1740,22 +1740,24 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da } } - fun markHidden(id: RecipientId, clearProfileKey: Boolean = false) { + fun markHidden(id: RecipientId, clearProfileKey: Boolean = false, showMessageRequest: Boolean = false) { val contentValues = if (clearProfileKey) { contentValuesOf( - HIDDEN to 1, + HIDDEN to if (showMessageRequest) Recipient.HiddenState.HIDDEN_MESSAGE_REQUEST.serialize() else Recipient.HiddenState.HIDDEN.serialize(), PROFILE_SHARING to 0, PROFILE_KEY to null ) } else { contentValuesOf( - HIDDEN to 1, + HIDDEN to if (showMessageRequest) Recipient.HiddenState.HIDDEN_MESSAGE_REQUEST.serialize() else Recipient.HiddenState.HIDDEN.serialize(), PROFILE_SHARING to 0 ) } val updated = writableDatabase.update(TABLE_NAME, contentValues, "$ID_WHERE AND $TYPE = ?", SqlUtil.buildArgs(id, RecipientType.INDIVIDUAL.id)) > 0 if (updated) { + SignalDatabase.distributionLists.removeMemberFromAllLists(id) + SignalDatabase.messages.deleteStoriesForRecipient(id) rotateStorageId(id) ApplicationDependencies.getDatabaseObserver().notifyRecipientChanged(id) StorageSyncHelper.scheduleSyncForDataChange() @@ -3033,7 +3035,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da fun getRegistered(): List { val results: MutableList = LinkedList() - readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$REGISTERED = ? and $HIDDEN = ?", arrayOf("1", "0"), null, null, null).use { cursor -> + readableDatabase.query(TABLE_NAME, ID_PROJECTION, "$REGISTERED = ? and $HIDDEN = ?", arrayOf("1", "${Recipient.HiddenState.NOT_HIDDEN.serialize()}"), null, null, null).use { cursor -> while (cursor != null && cursor.moveToNext()) { results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID)))) } @@ -3045,7 +3047,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da return readableDatabase .select(ID) .from(TABLE_NAME) - .where("$REGISTERED = ? and $HIDDEN = ? AND $ACI_COLUMN NOT NULL", 1, 0) + .where("$REGISTERED = ? and $HIDDEN = ? AND $ACI_COLUMN NOT NULL", 1, Recipient.HiddenState.NOT_HIDDEN.serialize()) .run() .readToSet { cursor -> RecipientId.from(cursor.requireLong(ID)) @@ -3078,7 +3080,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da return readableDatabase .select(E164) .from(TABLE_NAME) - .where("$REGISTERED = ? and $HIDDEN = ? AND $E164 NOT NULL", 1, 0) + .where("$REGISTERED = ? and $HIDDEN = ? AND $E164 NOT NULL", 1, Recipient.HiddenState.NOT_HIDDEN.serialize()) .run() .readToSet { cursor -> cursor.requireNonNullString(E164) @@ -4194,7 +4196,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da hasGroupsInCommon = cursor.requireBoolean(GROUPS_IN_COMMON), badges = parseBadgeList(cursor.requireBlob(BADGES)), needsPniSignature = cursor.requireBoolean(NEEDS_PNI_SIGNATURE), - isHidden = cursor.requireBoolean(HIDDEN), + hiddenState = Recipient.HiddenState.deserialize(cursor.requireInt(HIDDEN)), callLinkRoomId = cursor.requireString(CALL_LINK_ROOM_ID)?.let { CallLinkRoomId.DatabaseSerializer.deserialize(it) } ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt index 5c12585e1d..0446bad3c1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/RecipientRecord.kt @@ -75,7 +75,7 @@ data class RecipientRecord( val badges: List, @get:JvmName("needsPniSignature") val needsPniSignature: Boolean, - val isHidden: Boolean, + val hiddenState: Recipient.HiddenState, val callLinkRoomId: CallLinkRoomId? ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java index bb9f121245..59b19c489c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java @@ -145,10 +145,15 @@ public final class MessageRequestRepository { } else { if (RecipientUtil.isMessageRequestAccepted(context, threadId)) { return MessageRequestState.NONE; - } else if (RecipientUtil.isRecipientHidden(threadId)) { - return MessageRequestState.INDIVIDUAL_HIDDEN; } else { - return MessageRequestState.INDIVIDUAL; + Recipient.HiddenState hiddenState = RecipientUtil.getRecipientHiddenState(threadId); + if (hiddenState == Recipient.HiddenState.NOT_HIDDEN) { + return MessageRequestState.INDIVIDUAL; + } else if (hiddenState == Recipient.HiddenState.HIDDEN) { + return MessageRequestState.NONE_HIDDEN; + } else { + return MessageRequestState.INDIVIDUAL_HIDDEN; + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestState.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestState.java index 8604bb0e24..4451754b85 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestState.java @@ -7,6 +7,9 @@ public enum MessageRequestState { /** No message request necessary */ NONE, + /** No message request necessary as the user was hidden after accepting*/ + NONE_HIDDEN, + /** A user is blocked */ BLOCKED_INDIVIDUAL, diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt index 53c9bae541..0aadf21701 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/DataMessageProcessor.kt @@ -77,6 +77,7 @@ import org.thoughtcrime.securesms.mms.QuoteModel import org.thoughtcrime.securesms.mms.StickerSlide import org.thoughtcrime.securesms.notifications.v2.ConversationId import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.Recipient.HiddenState import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientUtil import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage @@ -169,6 +170,10 @@ object DataMessageProcessor { handleProfileKey(envelope.timestamp, message.profileKey.toByteArray(), senderRecipient) } + if (groupId == null && senderRecipient.hiddenState == HiddenState.HIDDEN) { + SignalDatabase.recipients.markHidden(senderRecipient.id, clearProfileKey = false, showMessageRequest = true) + } + if (metadata.sealedSender && messageId != null) { SignalExecutors.BOUNDED.execute { ApplicationDependencies.getJobManager().add(SendDeliveryReceiptJob(senderRecipient.id, message.timestamp, messageId)) } } else if (!metadata.sealedSender) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java index 233be951c3..c82ed7f7d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.java @@ -6,6 +6,7 @@ import android.net.Uri; import android.text.TextUtils; import androidx.annotation.AnyThread; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.WorkerThread; @@ -57,6 +58,8 @@ import org.whispersystems.signalservice.api.util.OptionalUtil; import org.whispersystems.signalservice.api.util.Preconditions; import org.whispersystems.signalservice.api.util.UuidUtil; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -116,7 +119,7 @@ public class Recipient { private final String profileAvatar; private final ProfileAvatarFileDetails profileAvatarFileDetails; private final boolean profileSharing; - private final boolean isHidden; + private final Recipient.HiddenState hiddenState; private final long lastProfileFetch; private final String notificationChannel; private final UnidentifiedAccessMode unidentifiedAccessMode; @@ -401,7 +404,7 @@ public class Recipient { this.profileAvatar = null; this.profileAvatarFileDetails = ProfileAvatarFileDetails.NO_DETAILS; this.profileSharing = false; - this.isHidden = false; + this.hiddenState = HiddenState.NOT_HIDDEN; this.lastProfileFetch = 0; this.notificationChannel = null; this.unidentifiedAccessMode = UnidentifiedAccessMode.DISABLED; @@ -456,7 +459,7 @@ public class Recipient { this.profileAvatar = details.profileAvatar; this.profileAvatarFileDetails = details.profileAvatarFileDetails; this.profileSharing = details.profileSharing; - this.isHidden = details.isHidden; + this.hiddenState = details.hiddenState; this.lastProfileFetch = details.lastProfileFetch; this.notificationChannel = details.notificationChannel; this.unidentifiedAccessMode = details.unidentifiedAccessMode; @@ -852,7 +855,11 @@ public class Recipient { } public boolean isHidden() { - return isHidden; + return hiddenState != HiddenState.NOT_HIDDEN; + } + + public Recipient.HiddenState getHiddenState() { + return hiddenState; } public long getLastProfileFetchTime() { @@ -1244,6 +1251,31 @@ public class Recipient { return Objects.hash(id); } + public enum HiddenState { + NOT_HIDDEN(0), + HIDDEN(1), + HIDDEN_MESSAGE_REQUEST(2); + + private final int value; + + HiddenState(int value) { + this.value = value; + } + + public int serialize() { + return value; + } + + public static HiddenState deserialize(int value) { + switch (value) { + case 0: return NOT_HIDDEN; + case 1: return HIDDEN; + case 2: return HIDDEN_MESSAGE_REQUEST; + default: throw new IllegalArgumentException(); + } + } + } + public enum Capability { UNKNOWN(0), SUPPORTED(1), @@ -1327,7 +1359,7 @@ public class Recipient { expireMessages == other.expireMessages && Objects.equals(profileAvatarFileDetails, other.profileAvatarFileDetails) && profileSharing == other.profileSharing && - isHidden == other.isHidden && + hiddenState == other.hiddenState && Objects.equals(aci, other.aci) && Objects.equals(username, other.username) && Objects.equals(e164, other.e164) && diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java index a9dfb8ba80..bdb74bfe96 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientDetails.java @@ -10,7 +10,6 @@ import org.signal.libsignal.zkgroup.profiles.ExpiringProfileKeyCredential; import org.thoughtcrime.securesms.badges.models.Badge; import org.thoughtcrime.securesms.conversation.colors.AvatarColor; import org.thoughtcrime.securesms.conversation.colors.ChatColors; -import org.thoughtcrime.securesms.database.RecipientTable.InsightsBannerTier; import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting; import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState; import org.thoughtcrime.securesms.database.RecipientTable.UnidentifiedAccessMode; @@ -64,7 +63,7 @@ public class RecipientDetails { final String profileAvatar; final ProfileAvatarFileDetails profileAvatarFileDetails; final boolean profileSharing; - final boolean isHidden; + final Recipient.HiddenState hiddenState; final boolean isActiveGroup; final long lastProfileFetch; final boolean systemContact; @@ -128,7 +127,7 @@ public class RecipientDetails { this.profileAvatar = record.getProfileAvatar(); this.profileAvatarFileDetails = record.getProfileAvatarFileDetails(); this.profileSharing = record.isProfileSharing(); - this.isHidden = record.isHidden(); + this.hiddenState = record.getHiddenState(); this.lastProfileFetch = record.getLastProfileFetch(); this.systemContact = systemContact; this.isSelf = isSelf; @@ -181,7 +180,7 @@ public class RecipientDetails { this.profileAvatar = null; this.profileAvatarFileDetails = ProfileAvatarFileDetails.NO_DETAILS; this.profileSharing = false; - this.isHidden = false; + this.hiddenState = Recipient.HiddenState.NOT_HIDDEN; this.lastProfileFetch = 0; this.systemContact = true; this.isSelf = false; diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java index d5e2621791..993a52b6cd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java @@ -198,6 +198,22 @@ public class RecipientUtil { StorageSyncHelper.scheduleSyncForDataChange(); } + @WorkerThread + public static Recipient.HiddenState getRecipientHiddenState(long threadId) { + if (threadId < 0) { + return Recipient.HiddenState.NOT_HIDDEN; + } + + ThreadTable threadTable = SignalDatabase.threads(); + Recipient threadRecipient = threadTable.getRecipientForThreadId(threadId); + + if (threadRecipient == null) { + return Recipient.HiddenState.NOT_HIDDEN; + } + + return threadRecipient.getHiddenState(); + } + @WorkerThread public static boolean isRecipientHidden(long threadId) { if (threadId < 0) { @@ -287,7 +303,7 @@ public class RecipientUtil { boolean firstMessage = SignalDatabase.messages().getOutgoingSecureMessageCount(threadId) == 0; - if (firstMessage) { + if (firstMessage || recipient.isHidden()) { SignalDatabase.recipients().setProfileSharing(recipient.getId(), true); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java index 224ff939a3..55f05c18ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java @@ -126,7 +126,7 @@ public final class StorageSyncModels { .setMuteUntil(recipient.getMuteUntil()) .setHideStory(hideStory) .setUnregisteredTimestamp(recipient.getSyncExtras().getUnregisteredTimestamp()) - .setHidden(recipient.isHidden()) + .setHidden(recipient.getHiddenState() != Recipient.HiddenState.NOT_HIDDEN) .setUsername(recipient.getUsername()) .build(); } diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt index 2ab5f539d1..73e8d6b21f 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/RecipientDatabaseTestUtils.kt @@ -150,7 +150,7 @@ object RecipientDatabaseTestUtils { hasGroupsInCommon, badges, needsPniSignature = false, - isHidden = false, + hiddenState = Recipient.HiddenState.NOT_HIDDEN, null ), participants,