diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index d7235a3044..ae4f740a2e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -32,6 +32,8 @@ import com.fasterxml.jackson.annotation.JsonProperty; import net.sqlcipher.database.SQLiteDatabase; import org.jsoup.helper.StringUtil; +import org.signal.zkgroup.InvalidInputException; +import org.signal.zkgroup.groups.GroupMasterKey; import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; @@ -39,6 +41,8 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord; +import org.thoughtcrime.securesms.groups.BadGroupIdException; +import org.thoughtcrime.securesms.groups.GroupId; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; @@ -758,6 +762,25 @@ public class ThreadDatabase extends Database { return 0; } + /** + * @return Pinned recipients, in order from top to bottom. + */ + public @NonNull List getPinnedRecipientIds() { + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String[] projection = new String[]{RECIPIENT_ID}; + String query = PINNED + " > ?"; + String[] args = SqlUtil.buildArgs(0); + List pinned = new LinkedList<>(); + + try (Cursor cursor = db.query(TABLE_NAME, projection, query, args, null, null, PINNED + " ASC")) { + while (cursor.moveToNext()) { + pinned.add(RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID))); + } + } + + return pinned; + } + public void pinConversations(@NonNull Set threadIds) { SQLiteDatabase db = databaseHelper.getWritableDatabase(); @@ -771,6 +794,7 @@ public class ThreadDatabase extends Database { contentValues.put(PINNED, ++pinnedCount); db.update(TABLE_NAME, contentValues, ID_WHERE, SqlUtil.buildArgs(threadId)); + } db.setTransactionSuccessful(); @@ -780,6 +804,9 @@ public class ThreadDatabase extends Database { } notifyConversationListListeners(); + + DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().getId()); + StorageSyncHelper.scheduleSyncForDataChange(); } public void unpinConversations(@NonNull Set threadIds) { @@ -792,6 +819,9 @@ public class ThreadDatabase extends Database { db.update(TABLE_NAME, contentValues, selection, SqlUtil.buildArgs(Stream.of(threadIds).toArray())); notifyConversationListListeners(); + + DatabaseFactory.getRecipientDatabase(context).markNeedsSync(Recipient.self().getId()); + StorageSyncHelper.scheduleSyncForDataChange(); } public void archiveConversation(long threadId) { @@ -1025,7 +1055,57 @@ public class ThreadDatabase extends Database { } public void applyStorageSyncUpdate(@NonNull RecipientId recipientId, @NonNull SignalAccountRecord record) { - applyStorageSyncUpdate(recipientId, record.isNoteToSelfArchived(), record.isNoteToSelfForcedUnread()); + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + + db.beginTransaction(); + try { + applyStorageSyncUpdate(recipientId, record.isNoteToSelfArchived(), record.isNoteToSelfForcedUnread()); + + ContentValues clearPinnedValues = new ContentValues(); + clearPinnedValues.put(PINNED, 0); + db.update(TABLE_NAME, clearPinnedValues, null, null); + + int pinnedPosition = 1; + for (SignalAccountRecord.PinnedConversation pinned : record.getPinnedConversations()) { + ContentValues pinnedValues = new ContentValues(); + pinnedValues.put(PINNED, pinnedPosition); + + Recipient pinnedRecipient; + + if (pinned.getContact().isPresent()) { + pinnedRecipient = Recipient.externalPush(context, pinned.getContact().get()); + } else if (pinned.getGroupV1Id().isPresent()) { + try { + pinnedRecipient = Recipient.externalGroup(context, GroupId.v1Exact(pinned.getGroupV1Id().get())); + } catch (BadGroupIdException e) { + Log.w(TAG, "Failed to parse pinned groupV1 ID!", e); + pinnedRecipient = null; + } + } else if (pinned.getGroupV2MasterKey().isPresent()) { + try { + pinnedRecipient = Recipient.externalGroup(context, GroupId.v2(new GroupMasterKey(pinned.getGroupV2MasterKey().get()))); + } catch (InvalidInputException e) { + Log.w(TAG, "Failed to parse pinned groupV2 master key!", e); + pinnedRecipient = null; + } + } else { + Log.w(TAG, "Empty pinned conversation on the AccountRecord?"); + pinnedRecipient = null; + } + + if (pinnedRecipient != null) { + db.update(TABLE_NAME, pinnedValues, RECIPIENT_ID + " = ?", SqlUtil.buildArgs(pinnedRecipient.getId())); + } + + pinnedPosition++; + } + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + notifyConversationListListeners(); } private void applyStorageSyncUpdate(@NonNull RecipientId recipientId, boolean archived, boolean forcedUnread) { 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 9b25d60264..337ae07876 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java @@ -88,7 +88,7 @@ final class MessageRequestRepository { } return MessageRequestState.REQUIRED; - } else if (FeatureFlags.modernProfileSharing() && !recipient.isPushV2Group() && !recipient.isProfileSharing()) { + } else if (FeatureFlags.modernProfileSharing() && !RecipientUtil.isLegacyProfileSharingAccepted(recipient)) { return MessageRequestState.REQUIRED; } else if (RecipientUtil.isPreMessageRequestThread(context, threadId) && !RecipientUtil.isLegacyProfileSharingAccepted(recipient)) { return MessageRequestState.PRE_MESSAGE_REQUEST; 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 270db4cdd7..dd6ce37712 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientUtil.java @@ -256,7 +256,8 @@ public class RecipientUtil { return threadRecipient.isLocalNumber() || threadRecipient.isProfileSharing() || threadRecipient.isSystemContact() || - !threadRecipient.isRegistered(); + !threadRecipient.isRegistered() || + threadRecipient.isForceSmsSelection(); } @WorkerThread diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java index 8a5ed24945..1ae2cee952 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountConflictMerger.java @@ -6,11 +6,13 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.logging.Log; import org.whispersystems.libsignal.util.guava.Optional; import org.whispersystems.signalservice.api.storage.SignalAccountRecord; +import org.whispersystems.signalservice.api.storage.SignalAccountRecord.PinnedConversation; import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Set; @@ -66,9 +68,10 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger pinnedConversations = remote.getPinnedConversations(); AccountRecord.PhoneNumberSharingMode phoneNumberSharingMode = remote.getPhoneNumberSharingMode(); - boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted); - boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted ); + boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations); + boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations); if (matchesRemote) { return remote; @@ -90,6 +93,7 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger pinnedConversations) { return Arrays.equals(contact.serializeUnknownFields(), unknownFields) && Objects.equals(contact.getGivenName().or(""), givenName) && @@ -121,6 +126,7 @@ class AccountConflictMerger implements StorageSyncHelper.ConflictMerger pinned = Stream.of(DatabaseFactory.getThreadDatabase(context).getPinnedRecipientIds()) + .map(recipientDatabase::getRecipientSettingsForSync) + .toList(); SignalAccountRecord account = new SignalAccountRecord.Builder(self.getStorageServiceId()) .setUnknownFields(settings != null ? settings.getSyncExtras().getStorageProto() : null) @@ -427,30 +431,13 @@ public final class StorageSyncHelper { .setSealedSenderIndicatorsEnabled(TextSecurePreferences.isShowUnidentifiedDeliveryIndicatorsEnabled(context)) .setLinkPreviewsEnabled(SignalStore.settings().isLinkPreviewsEnabled()) .setUnlistedPhoneNumber(SignalStore.phoneNumberPrivacy().getPhoneNumberListingMode().isUnlisted()) - .setPhoneNumberSharingMode(localToRemotePhoneNumberSharingMode(SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode())) + .setPhoneNumberSharingMode(StorageSyncModels.localToRemotePhoneNumberSharingMode(SignalStore.phoneNumberPrivacy().getPhoneNumberSharingMode())) + .setPinnedConversations(StorageSyncModels.localToRemotePinnedConversations(pinned)) .build(); return SignalStorageRecord.forAccount(account); } - private static AccountRecord.PhoneNumberSharingMode localToRemotePhoneNumberSharingMode(PhoneNumberPrivacyValues.PhoneNumberSharingMode phoneNumberPhoneNumberSharingMode) { - switch (phoneNumberPhoneNumberSharingMode) { - case EVERYONE: return AccountRecord.PhoneNumberSharingMode.EVERYBODY; - case CONTACTS: return AccountRecord.PhoneNumberSharingMode.CONTACTS_ONLY; - case NOBODY : return AccountRecord.PhoneNumberSharingMode.NOBODY; - default : throw new AssertionError(); - } - } - - private static PhoneNumberPrivacyValues.PhoneNumberSharingMode remoteToLocalPhoneNumberSharingMode(AccountRecord.PhoneNumberSharingMode phoneNumberPhoneNumberSharingMode) { - switch (phoneNumberPhoneNumberSharingMode) { - case EVERYBODY : return PhoneNumberPrivacyValues.PhoneNumberSharingMode.EVERYONE; - case CONTACTS_ONLY: return PhoneNumberPrivacyValues.PhoneNumberSharingMode.CONTACTS; - case NOBODY : return PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY; - default : return PhoneNumberPrivacyValues.PhoneNumberSharingMode.CONTACTS; - } - } - public static void applyAccountStorageSyncUpdates(@NonNull Context context, Optional> update) { if (!update.isPresent()) { return; @@ -466,7 +453,7 @@ public final class StorageSyncHelper { TextSecurePreferences.setShowUnidentifiedDeliveryIndicatorsEnabled(context, update.isSealedSenderIndicatorsEnabled()); SignalStore.settings().setLinkPreviewsEnabled(update.isLinkPreviewsEnabled()); SignalStore.phoneNumberPrivacy().setPhoneNumberListingMode(update.isPhoneNumberUnlisted() ? PhoneNumberPrivacyValues.PhoneNumberListingMode.UNLISTED : PhoneNumberPrivacyValues.PhoneNumberListingMode.LISTED); - SignalStore.phoneNumberPrivacy().setPhoneNumberSharingMode(remoteToLocalPhoneNumberSharingMode(update.getPhoneNumberSharingMode())); + SignalStore.phoneNumberPrivacy().setPhoneNumberSharingMode(StorageSyncModels.remoteToLocalPhoneNumberSharingMode(update.getPhoneNumberSharingMode())); if (fetchProfile && update.getAvatarUrlPath().isPresent()) { ApplicationDependencies.getJobManager().add(new RetrieveProfileAvatarJob(Recipient.self(), update.getAvatarUrlPath().get())); 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 6084c91fae..ce30b33d64 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/StorageSyncModels.java @@ -2,18 +2,26 @@ package org.thoughtcrime.securesms.storage; import androidx.annotation.NonNull; +import com.annimon.stream.Stream; + import org.signal.zkgroup.groups.GroupMasterKey; import org.thoughtcrime.securesms.database.IdentityDatabase; +import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; import org.thoughtcrime.securesms.groups.GroupId; +import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.whispersystems.signalservice.api.push.SignalServiceAddress; +import org.whispersystems.signalservice.api.storage.SignalAccountRecord; import org.whispersystems.signalservice.api.storage.SignalContactRecord; import org.whispersystems.signalservice.api.storage.SignalGroupV1Record; import org.whispersystems.signalservice.api.storage.SignalGroupV2Record; import org.whispersystems.signalservice.api.storage.SignalStorageRecord; +import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; import org.whispersystems.signalservice.internal.storage.protos.ContactRecord.IdentityState; +import java.util.List; import java.util.Set; public final class StorageSyncModels { @@ -37,6 +45,42 @@ public final class StorageSyncModels { } } + public static AccountRecord.PhoneNumberSharingMode localToRemotePhoneNumberSharingMode(PhoneNumberPrivacyValues.PhoneNumberSharingMode phoneNumberPhoneNumberSharingMode) { + switch (phoneNumberPhoneNumberSharingMode) { + case EVERYONE: return AccountRecord.PhoneNumberSharingMode.EVERYBODY; + case CONTACTS: return AccountRecord.PhoneNumberSharingMode.CONTACTS_ONLY; + case NOBODY : return AccountRecord.PhoneNumberSharingMode.NOBODY; + default : throw new AssertionError(); + } + } + + public static PhoneNumberPrivacyValues.PhoneNumberSharingMode remoteToLocalPhoneNumberSharingMode(AccountRecord.PhoneNumberSharingMode phoneNumberPhoneNumberSharingMode) { + switch (phoneNumberPhoneNumberSharingMode) { + case EVERYBODY : return PhoneNumberPrivacyValues.PhoneNumberSharingMode.EVERYONE; + case CONTACTS_ONLY: return PhoneNumberPrivacyValues.PhoneNumberSharingMode.CONTACTS; + case NOBODY : return PhoneNumberPrivacyValues.PhoneNumberSharingMode.NOBODY; + default : return PhoneNumberPrivacyValues.PhoneNumberSharingMode.CONTACTS; + } + } + + public static List localToRemotePinnedConversations(@NonNull List settings) { + return Stream.of(settings) + .filter(s -> s.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V1 || + s.getGroupType() == RecipientDatabase.GroupType.SIGNAL_V2 || + s.getRegistered() == RecipientDatabase.RegisteredState.REGISTERED) + .map(StorageSyncModels::localToRemotePinnedConversation) + .toList(); + } + + private static @NonNull SignalAccountRecord.PinnedConversation localToRemotePinnedConversation(@NonNull RecipientSettings settings) { + switch (settings.getGroupType()) { + case NONE : return SignalAccountRecord.PinnedConversation.forContact(new SignalServiceAddress(settings.getUuid(), settings.getE164())); + case SIGNAL_V1: return SignalAccountRecord.PinnedConversation.forGroupV1(settings.getGroupId().requireV1().getDecodedId()); + case SIGNAL_V2: return SignalAccountRecord.PinnedConversation.forGroupV2(settings.getSyncExtras().getGroupMasterKey().serialize()); + default : throw new AssertionError("Unexpected group type!"); + } + } + private static @NonNull SignalContactRecord localToRemoteContact(@NonNull RecipientSettings recipient, byte[] rawStorageId) { if (recipient.getUuid() == null && recipient.getE164() == null) { throw new AssertionError("Must have either a UUID or a phone number!"); diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java index b259e726c7..decaec341d 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/storage/SignalAccountRecord.java @@ -3,10 +3,14 @@ package org.whispersystems.signalservice.api.storage; import com.google.protobuf.ByteString; import org.whispersystems.libsignal.util.guava.Optional; +import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.OptionalUtil; import org.whispersystems.signalservice.api.util.ProtoUtil; +import org.whispersystems.signalservice.api.util.UuidUtil; import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; public final class SignalAccountRecord implements SignalRecord { @@ -15,20 +19,26 @@ public final class SignalAccountRecord implements SignalRecord { private final AccountRecord proto; private final boolean hasUnknownFields; - private final Optional givenName; - private final Optional familyName; - private final Optional avatarUrlPath; - private final Optional profileKey; + private final Optional givenName; + private final Optional familyName; + private final Optional avatarUrlPath; + private final Optional profileKey; + private final List pinnedConversations; public SignalAccountRecord(StorageId id, AccountRecord proto) { this.id = id; this.proto = proto; this.hasUnknownFields = ProtoUtil.hasUnknownFields(proto); - this.givenName = OptionalUtil.absentIfEmpty(proto.getGivenName()); - this.familyName = OptionalUtil.absentIfEmpty(proto.getFamilyName()); - this.profileKey = OptionalUtil.absentIfEmpty(proto.getProfileKey()); - this.avatarUrlPath = OptionalUtil.absentIfEmpty(proto.getAvatarUrlPath()); + this.givenName = OptionalUtil.absentIfEmpty(proto.getGivenName()); + this.familyName = OptionalUtil.absentIfEmpty(proto.getFamilyName()); + this.profileKey = OptionalUtil.absentIfEmpty(proto.getProfileKey()); + this.avatarUrlPath = OptionalUtil.absentIfEmpty(proto.getAvatarUrlPath()); + this.pinnedConversations = new ArrayList<>(proto.getPinnedConversationsCount()); + + for (AccountRecord.PinnedConversation conversation : proto.getPinnedConversationsList()) { + pinnedConversations.add(PinnedConversation.fromRemote(conversation)); + } } @Override @@ -92,6 +102,10 @@ public final class SignalAccountRecord implements SignalRecord { return proto.getUnlistedPhoneNumber(); } + public List getPinnedConversations() { + return pinnedConversations; + } + AccountRecord toProto() { return proto; } @@ -110,6 +124,96 @@ public final class SignalAccountRecord implements SignalRecord { return Objects.hash(id, proto); } + public static class PinnedConversation { + private final Optional contact; + private final Optional groupV1Id; + private final Optional groupV2MasterKey; + + private PinnedConversation(Optional contact, Optional groupV1Id, Optional groupV2MasterKey) { + this.contact = contact; + this.groupV1Id = groupV1Id; + this.groupV2MasterKey = groupV2MasterKey; + } + + public static PinnedConversation forContact(SignalServiceAddress address) { + return new PinnedConversation(Optional.of(address), Optional.absent(), Optional.absent()); + } + + public static PinnedConversation forGroupV1(byte[] groupId) { + return new PinnedConversation(Optional.absent(), Optional.of(groupId), Optional.absent()); + } + + public static PinnedConversation forGroupV2(byte[] masterKey) { + return new PinnedConversation(Optional.absent(), Optional.absent(), Optional.of(masterKey)); + } + + private static PinnedConversation forEmpty() { + return new PinnedConversation(Optional.absent(), Optional.absent(), Optional.absent()); + } + + static PinnedConversation fromRemote(AccountRecord.PinnedConversation remote) { + if (remote.hasContact()) { + return forContact(new SignalServiceAddress(UuidUtil.parseOrNull(remote.getContact().getUuid()), remote.getContact().getE164())); + } else if (!remote.getLegacyGroupId().isEmpty()) { + return forGroupV1(remote.getLegacyGroupId().toByteArray()); + } else if (!remote.getGroupMasterKey().isEmpty()) { + return forGroupV2(remote.getGroupMasterKey().toByteArray()); + } else { + return PinnedConversation.forEmpty(); + } + } + + public Optional getContact() { + return contact; + } + + public Optional getGroupV1Id() { + return groupV1Id; + } + + public Optional getGroupV2MasterKey() { + return groupV2MasterKey; + } + + public boolean isValid() { + return contact.isPresent() || groupV1Id.isPresent() || groupV2MasterKey.isPresent(); + } + + private AccountRecord.PinnedConversation toRemote() { + if (contact.isPresent()) { + AccountRecord.PinnedConversation.Contact.Builder contactBuilder = AccountRecord.PinnedConversation.Contact.newBuilder(); + if (contact.get().getUuid().isPresent()) { + contactBuilder.setUuid(contact.get().getUuid().get().toString()); + } + if (contact.get().getNumber().isPresent()) { + contactBuilder.setE164(contact.get().getNumber().get()); + } + return AccountRecord.PinnedConversation.newBuilder().setContact(contactBuilder.build()).build(); + } else if (groupV1Id.isPresent()) { + return AccountRecord.PinnedConversation.newBuilder().setLegacyGroupId(ByteString.copyFrom(groupV1Id.get())).build(); + } else if (groupV2MasterKey.isPresent()) { + return AccountRecord.PinnedConversation.newBuilder().setGroupMasterKey(ByteString.copyFrom(groupV2MasterKey.get())).build(); + } else { + return AccountRecord.PinnedConversation.newBuilder().build(); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PinnedConversation that = (PinnedConversation) o; + return contact.equals(that.contact) && + groupV1Id.equals(that.groupV1Id) && + groupV2MasterKey.equals(that.groupV2MasterKey); + } + + @Override + public int hashCode() { + return Objects.hash(contact, groupV1Id, groupV2MasterKey); + } + } + public static final class Builder { private final StorageId id; private final AccountRecord.Builder builder; @@ -186,6 +290,16 @@ public final class SignalAccountRecord implements SignalRecord { return this; } + public Builder setPinnedConversations(List pinnedConversations) { + builder.clearPinnedConversations(); + + for (PinnedConversation pinned : pinnedConversations) { + builder.addPinnedConversations(pinned.toRemote()); + } + + return this; + } + public SignalAccountRecord build() { AccountRecord proto = builder.build(); diff --git a/libsignal/service/src/main/proto/SignalStorage.proto b/libsignal/service/src/main/proto/SignalStorage.proto index c679a3afd9..a614316d7e 100644 --- a/libsignal/service/src/main/proto/SignalStorage.proto +++ b/libsignal/service/src/main/proto/SignalStorage.proto @@ -107,17 +107,31 @@ message AccountRecord { NOBODY = 2; } - bytes profileKey = 1; - string givenName = 2; - string familyName = 3; - string avatarUrlPath = 4; - bool noteToSelfArchived = 5; - bool readReceipts = 6; - bool sealedSenderIndicators = 7; - bool typingIndicators = 8; - bool proxiedLinkPreviews = 9; - bool noteToSelfMarkedUnread = 10; - bool linkPreviews = 11; - PhoneNumberSharingMode phoneNumberSharingMode = 12; - bool unlistedPhoneNumber = 13; + message PinnedConversation { + message Contact { + string uuid = 1; + string e164 = 2; + } + + oneof identifier { + Contact contact = 1; + bytes legacyGroupId = 3; + bytes groupMasterKey = 4; + } + } + + bytes profileKey = 1; + string givenName = 2; + string familyName = 3; + string avatarUrlPath = 4; + bool noteToSelfArchived = 5; + bool readReceipts = 6; + bool sealedSenderIndicators = 7; + bool typingIndicators = 8; + bool proxiedLinkPreviews = 9; + bool noteToSelfMarkedUnread = 10; + bool linkPreviews = 11; + PhoneNumberSharingMode phoneNumberSharingMode = 12; + bool unlistedPhoneNumber = 13; + repeated PinnedConversation pinnedConversations = 14; }