diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java index 1f7e22f19..56b0a0544 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ParentStoryId; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.StoryResult; +import org.thoughtcrime.securesms.database.model.StoryType; import org.thoughtcrime.securesms.database.model.StoryViewState; import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState; import org.thoughtcrime.securesms.database.model.databaseprotos.ThreadMergeEvent; @@ -135,7 +136,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns, public abstract void markGiftRedemptionStarted(long messageId); public abstract void markGiftRedemptionFailed(long messageId); - public abstract Set incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, boolean storiesOnly); + public abstract Set incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, @NonNull MessageQualifier messageType); public abstract List setEntireThreadRead(long threadId); public abstract List setMessagesReadSince(long threadId, long timestamp); @@ -218,6 +219,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns, public abstract void deleteGroupStoryReplies(long parentStoryId); public abstract boolean isOutgoingStoryAlreadyInDatabase(@NonNull RecipientId recipientId, long sentTimestamp); public abstract @NonNull List setGroupStoryMessagesReadSince(long threadId, long groupStoryId, long sinceTimestamp); + public abstract @NonNull List getStoryTypes(@NonNull List messageIds); public abstract @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId); public abstract void updateViewedStories(@NonNull Set syncMessageIds); @@ -920,4 +922,23 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns, return dateReceived; } } + + /** + * Describes which messages to act on. This is used when incrementing receipts. + * Specifically, this was added to support stories having separate viewed receipt settings. + */ + public enum MessageQualifier { + /** + * A normal database message (i.e. not a story) + */ + NORMAL, + /** + * A story message + */ + STORY, + /** + * Both normal and story message + */ + ALL + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index f5e69642f..4b403bb5e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -379,7 +379,7 @@ public class MmsDatabase extends MessageDatabase { @Override public @NonNull List getViewedIncomingMessages(long threadId) { SQLiteDatabase db = databaseHelper.getSignalReadableDatabase(); - String[] columns = new String[]{ID, RECIPIENT_ID, DATE_SENT, MESSAGE_BOX, THREAD_ID}; + String[] columns = new String[]{ID, RECIPIENT_ID, DATE_SENT, MESSAGE_BOX, THREAD_ID, STORY_TYPE}; String where = THREAD_ID + " = ? AND " + VIEWED_RECEIPT_COUNT + " > 0 AND " + MESSAGE_BOX + " & " + Types.BASE_INBOX_TYPE + " = " + Types.BASE_INBOX_TYPE; String[] args = SqlUtil.buildArgs(threadId); @@ -395,6 +395,7 @@ public class MmsDatabase extends MessageDatabase { RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); + StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), null)); } @@ -421,7 +422,7 @@ public class MmsDatabase extends MessageDatabase { } SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); - String[] columns = new String[]{ID, RECIPIENT_ID, DATE_SENT, MESSAGE_BOX, THREAD_ID}; + String[] columns = new String[]{ID, RECIPIENT_ID, DATE_SENT, MESSAGE_BOX, THREAD_ID, STORY_TYPE}; String where = ID + " IN (" + Util.join(messageIds, ",") + ") AND " + VIEWED_RECEIPT_COUNT + " = 0"; List results = new LinkedList<>(); @@ -435,6 +436,7 @@ public class MmsDatabase extends MessageDatabase { RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); + StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), null)); @@ -463,7 +465,7 @@ public class MmsDatabase extends MessageDatabase { @Override public @NonNull List setOutgoingGiftsRevealed(@NonNull List messageIds) { - String[] projection = SqlUtil.buildArgs(ID, RECIPIENT_ID, DATE_SENT, THREAD_ID); + String[] projection = SqlUtil.buildArgs(ID, RECIPIENT_ID, DATE_SENT, THREAD_ID, STORY_TYPE); String where = ID + " IN (" + Util.join(messageIds, ",") + ") AND (" + getOutgoingTypeClause() + ") AND (" + getTypeField() + " & " + Types.SPECIAL_TYPES_MASK + " = " + Types.SPECIAL_TYPE_GIFT_BADGE + ") AND " + VIEWED_RECEIPT_COUNT + " = 0"; List results = new LinkedList<>(); @@ -475,6 +477,7 @@ public class MmsDatabase extends MessageDatabase { RecipientId recipientId = RecipientId.from(CursorUtil.requireLong(cursor, RECIPIENT_ID)); long dateSent = CursorUtil.requireLong(cursor, DATE_SENT); SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); + StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); results.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), null)); @@ -1122,13 +1125,28 @@ public class MmsDatabase extends MessageDatabase { } @Override - public Set incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, boolean storiesOnly) { + public Set incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, MessageQualifier messageQualifier) { SQLiteDatabase database = databaseHelper.getSignalWritableDatabase(); Set messageUpdates = new HashSet<>(); + final String qualifierWhere; + switch (messageQualifier) { + case NORMAL: + qualifierWhere = " AND NOT (" + IS_STORY_CLAUSE + ")"; + break; + case STORY: + qualifierWhere = " AND " + IS_STORY_CLAUSE; + break; + case ALL: + qualifierWhere = ""; + break; + default: + throw new IllegalArgumentException("Unsupported qualifier: " + messageQualifier); + } + try (Cursor cursor = SQLiteDatabaseExtensionsKt.select(database, ID, THREAD_ID, MESSAGE_BOX, RECIPIENT_ID, receiptType.getColumnName(), RECEIPT_TIMESTAMP) .from(TABLE_NAME) - .where(DATE_SENT + " = ?" + (storiesOnly ? " AND " + IS_STORY_CLAUSE : ""), messageId.getTimetamp()) + .where(DATE_SENT + " = ?" + qualifierWhere, messageId.getTimetamp()) .run()) { while (cursor.moveToNext()) { @@ -1509,6 +1527,38 @@ public class MmsDatabase extends MessageDatabase { } } + @Override + public @NonNull List getStoryTypes(@NonNull List messageIds) { + List mmsMessages = messageIds.stream() + .filter(MessageId::isMms) + .map(MessageId::getId) + .collect(java.util.stream.Collectors.toList()); + + if (mmsMessages.isEmpty()) { + return Collections.emptyList(); + } + + String[] projection = SqlUtil.buildArgs(ID, STORY_TYPE); + List queries = SqlUtil.buildCollectionQuery(ID, mmsMessages); + HashMap storyTypes = new HashMap<>(); + + for (final SqlUtil.Query query : queries) { + try (Cursor cursor = getWritableDatabase().query(TABLE_NAME, projection, query.getWhere(), query.getWhereArgs(), null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + storyTypes.put(CursorUtil.requireLong(cursor, ID), StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE))); + } + } + } + + return messageIds.stream().map(id -> { + if (id.isMms() && storyTypes.containsKey(id.getId())) { + return storyTypes.get(id.getId()); + } else { + return StoryType.NONE; + } + }).collect(java.util.stream.Collectors.toList()); + } + @Override public List setEntireThreadRead(long threadId) { return setMessagesRead(THREAD_ID + " = ? AND " + STORY_TYPE + " = 0 AND " + PARENT_STORY_ID + " <= 0", new String[] { String.valueOf(threadId)}); @@ -1528,7 +1578,7 @@ public class MmsDatabase extends MessageDatabase { database.beginTransaction(); try { - cursor = database.query(TABLE_NAME, new String[] {ID, RECIPIENT_ID, DATE_SENT, MESSAGE_BOX, EXPIRES_IN, EXPIRE_STARTED, THREAD_ID }, where, arguments, null, null, null); + cursor = database.query(TABLE_NAME, new String[] {ID, RECIPIENT_ID, DATE_SENT, MESSAGE_BOX, EXPIRES_IN, EXPIRE_STARTED, THREAD_ID, STORY_TYPE }, where, arguments, null, null, null); while(cursor != null && cursor.moveToNext()) { if (Types.isSecureType(CursorUtil.requireLong(cursor, MESSAGE_BOX))) { @@ -1540,6 +1590,7 @@ public class MmsDatabase extends MessageDatabase { long expireStarted = CursorUtil.requireLong(cursor, EXPIRE_STARTED); SyncMessageId syncMessageId = new SyncMessageId(recipientId, dateSent); ExpirationInfo expirationInfo = new ExpirationInfo(messageId, expiresIn, expireStarted, true); + StoryType storyType = StoryType.fromCode(CursorUtil.requireInt(cursor, STORY_TYPE)); if (!recipientId.equals(releaseChannelId)) { result.add(new MarkedMessageInfo(threadId, syncMessageId, new MessageId(messageId, true), expirationInfo)); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index dd6761710..b1faceb57 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -493,6 +493,10 @@ public class MmsSmsDatabase extends Database { return incrementReceiptCounts(syncMessageIds, timestamp, MessageDatabase.ReceiptType.VIEWED); } + public @NonNull Collection incrementViewedNonStoryReceiptCounts(@NonNull List syncMessageIds, long timestamp) { + return incrementReceiptCounts(syncMessageIds, timestamp, MessageDatabase.ReceiptType.VIEWED, MessageDatabase.MessageQualifier.NORMAL); + } + public boolean incrementViewedReceiptCount(SyncMessageId syncMessageId, long timestamp) { return incrementReceiptCount(syncMessageId, timestamp, MessageDatabase.ReceiptType.VIEWED); } @@ -505,7 +509,7 @@ public class MmsSmsDatabase extends Database { db.beginTransaction(); try { for (SyncMessageId id : syncMessageIds) { - Set updates = incrementStoryReceiptCountInternal(id, timestamp, MessageDatabase.ReceiptType.VIEWED); + Set updates = incrementReceiptCountInternal(id, timestamp, MessageDatabase.ReceiptType.VIEWED, MessageDatabase.MessageQualifier.STORY); if (updates.size() > 0) { messageUpdates.addAll(updates); @@ -537,13 +541,17 @@ public class MmsSmsDatabase extends Database { * @return Whether or not some thread was updated. */ private boolean incrementReceiptCount(SyncMessageId syncMessageId, long timestamp, @NonNull MessageDatabase.ReceiptType receiptType) { + return incrementReceiptCount(syncMessageId, timestamp, receiptType, MessageDatabase.MessageQualifier.ALL); + } + + private boolean incrementReceiptCount(SyncMessageId syncMessageId, long timestamp, @NonNull MessageDatabase.ReceiptType receiptType, @NonNull MessageDatabase.MessageQualifier messageQualifier) { SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); ThreadDatabase threadDatabase = SignalDatabase.threads(); Set messageUpdates = new HashSet<>(); db.beginTransaction(); try { - messageUpdates = incrementReceiptCountInternal(syncMessageId, timestamp, receiptType); + messageUpdates = incrementReceiptCountInternal(syncMessageId, timestamp, receiptType, messageQualifier); for (MessageUpdate messageUpdate : messageUpdates) { threadDatabase.update(messageUpdate.getThreadId(), false); @@ -567,6 +575,10 @@ public class MmsSmsDatabase extends Database { * @return All of the messages that didn't result in updates. */ private @NonNull Collection incrementReceiptCounts(@NonNull List syncMessageIds, long timestamp, @NonNull MessageDatabase.ReceiptType receiptType) { + return incrementReceiptCounts(syncMessageIds, timestamp, receiptType, MessageDatabase.MessageQualifier.ALL); + } + + private @NonNull Collection incrementReceiptCounts(@NonNull List syncMessageIds, long timestamp, @NonNull MessageDatabase.ReceiptType receiptType, @NonNull MessageDatabase.MessageQualifier messageQualifier) { SQLiteDatabase db = databaseHelper.getSignalWritableDatabase(); ThreadDatabase threadDatabase = SignalDatabase.threads(); Set messageUpdates = new HashSet<>(); @@ -575,7 +587,7 @@ public class MmsSmsDatabase extends Database { db.beginTransaction(); try { for (SyncMessageId id : syncMessageIds) { - Set updates = incrementReceiptCountInternal(id, timestamp, receiptType); + Set updates = incrementReceiptCountInternal(id, timestamp, receiptType, messageQualifier); if (updates.size() > 0) { messageUpdates.addAll(updates); @@ -609,22 +621,15 @@ public class MmsSmsDatabase extends Database { /** * Doesn't do any transactions or updates, so we can re-use the method safely. */ - private @NonNull Set incrementReceiptCountInternal(SyncMessageId syncMessageId, long timestamp, MessageDatabase.ReceiptType receiptType) { + private @NonNull Set incrementReceiptCountInternal(SyncMessageId syncMessageId, long timestamp, MessageDatabase.ReceiptType receiptType, @NonNull MessageDatabase.MessageQualifier messageQualifier) { Set messageUpdates = new HashSet<>(); - messageUpdates.addAll(SignalDatabase.sms().incrementReceiptCount(syncMessageId, timestamp, receiptType, false)); - messageUpdates.addAll(SignalDatabase.mms().incrementReceiptCount(syncMessageId, timestamp, receiptType, false)); + messageUpdates.addAll(SignalDatabase.sms().incrementReceiptCount(syncMessageId, timestamp, receiptType, messageQualifier)); + messageUpdates.addAll(SignalDatabase.mms().incrementReceiptCount(syncMessageId, timestamp, receiptType, messageQualifier)); return messageUpdates; } - /** - * Doesn't do any transactions or updates, so we can re-use the method safely. - */ - private @NonNull Set incrementStoryReceiptCountInternal(@NonNull SyncMessageId syncMessageId, long timestamp, @NonNull MessageDatabase.ReceiptType receiptType) { - return SignalDatabase.mms().incrementReceiptCount(syncMessageId, timestamp, receiptType, true); - } - public void updateViewedStories(@NonNull Set syncMessageIds) { SignalDatabase.mms().updateViewedStories(syncMessageIds); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 0dabf3dc1..00f857137 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -50,6 +50,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.ParentStoryId; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.StoryResult; +import org.thoughtcrime.securesms.database.model.StoryType; import org.thoughtcrime.securesms.database.model.StoryViewState; import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails; import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState; @@ -515,7 +516,11 @@ public class SmsDatabase extends MessageDatabase { } @Override - public @NonNull Set incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, boolean storiesOnly) { + public @NonNull Set incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, @NonNull MessageQualifier messageQualifier) { + if (messageQualifier == MessageQualifier.STORY) { + return Collections.emptySet(); + } + if (receiptType == ReceiptType.VIEWED) { return Collections.emptySet(); } @@ -1567,6 +1572,11 @@ public class SmsDatabase extends MessageDatabase { throw new UnsupportedOperationException(); } + @Override + public @NonNull List getStoryTypes(@NonNull List messageIds) { + throw new UnsupportedOperationException(); + } + @Override public void deleteGroupStoryReplies(long parentStoryId) { throw new UnsupportedOperationException(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Data.java b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Data.java index 207dedbf4..b27280835 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Data.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobmanager/Data.java @@ -120,6 +120,18 @@ public class Data { return integerArrays.get(key); } + public List getIntegerArrayAsList(@NonNull String key) { + throwIfAbsent(integerArrays, key); + + int[] array = Objects.requireNonNull(integerArrays.get(key)); + List ints = new ArrayList<>(array.length); + + for (int l : array) { + ints.add(l); + } + + return ints; + } public boolean hasLong(@NonNull String key) { return longs.containsKey(key); @@ -295,6 +307,17 @@ public class Data { return this; } + public Builder putIntegerListAsArray(@NonNull String key, @NonNull List value) { + int[] ints = new int[value.size()]; + + for (int i = 0; i < value.size(); i++) { + ints[i] = value.get(i); + } + + integerArrays.put(key, ints); + return this; + } + public Builder putInt(@NonNull String key, int value) { integers.put(key, value); return this; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 4549772b5..2e90c6090 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -62,6 +62,7 @@ import org.thoughtcrime.securesms.migrations.StickerMyDailyLifeMigrationJob; import org.thoughtcrime.securesms.migrations.StorageCapabilityMigrationJob; import org.thoughtcrime.securesms.migrations.StorageServiceMigrationJob; import org.thoughtcrime.securesms.migrations.StorageServiceSystemNameMigrationJob; +import org.thoughtcrime.securesms.migrations.StoryViewedReceiptsStateMigrationJob; import org.thoughtcrime.securesms.migrations.SyncDistributionListsMigrationJob; import org.thoughtcrime.securesms.migrations.TrimByLengthSettingsMigrationJob; import org.thoughtcrime.securesms.migrations.UserNotificationMigrationJob; @@ -225,6 +226,7 @@ public final class JobManagerFactories { put(StorageCapabilityMigrationJob.KEY, new StorageCapabilityMigrationJob.Factory()); put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory()); put(StorageServiceSystemNameMigrationJob.KEY, new StorageServiceSystemNameMigrationJob.Factory()); + put(StoryViewedReceiptsStateMigrationJob.KEY, new StoryViewedReceiptsStateMigrationJob.Factory()); put(TrimByLengthSettingsMigrationJob.KEY, new TrimByLengthSettingsMigrationJob.Factory()); put(UserNotificationMigrationJob.KEY, new UserNotificationMigrationJob.Factory()); put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java index d1bfd0d9d..a5c70c18f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SendViewedReceiptJob.java @@ -11,11 +11,13 @@ import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil; import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo; import org.thoughtcrime.securesms.database.SignalDatabase; import org.thoughtcrime.securesms.database.model.MessageId; +import org.thoughtcrime.securesms.database.model.StoryType; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -33,6 +35,7 @@ import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedExcept import java.io.IOException; import java.util.Collections; +import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -64,10 +67,10 @@ public class SendViewedReceiptJob extends BaseJob { private SendViewedReceiptJob(long threadId, @NonNull RecipientId recipientId, @NonNull List messageSentTimestamps, @NonNull List messageIds) { this(new Parameters.Builder() - .addConstraint(NetworkConstraint.KEY) - .setLifespan(TimeUnit.DAYS.toMillis(1)) - .setMaxAttempts(Parameters.UNLIMITED) - .build(), + .addConstraint(NetworkConstraint.KEY) + .setLifespan(TimeUnit.DAYS.toMillis(1)) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), threadId, recipientId, SendReadReceiptJob.ensureSize(messageSentTimestamps, MAX_TIMESTAMPS), @@ -130,15 +133,36 @@ public class SendViewedReceiptJob extends BaseJob { @Override public void onRun() throws IOException, UntrustedIdentityException { + + boolean canSendNonStoryReceipts = TextSecurePreferences.isReadReceiptsEnabled(context); + boolean canSendStoryReceipts = SignalStore.storyValues().getViewedReceiptsEnabled(); + + List messageIds = new LinkedList<>(); + List messageSentTimestamps = new LinkedList<>(); + List storyTypes = SignalDatabase.mms().getStoryTypes(messageIds); + + for (int i = 0; i < storyTypes.size(); i++) { + StoryType storyType = storyTypes.get(i); + if ((storyType == StoryType.NONE && canSendNonStoryReceipts) || (storyType.isStory() && canSendStoryReceipts)) { + messageIds.add(this.messageIds.get(i)); + messageSentTimestamps.add(this.messageSentTimestamps.get(i)); + } + } + if (!Recipient.self().isRegistered()) { throw new NotPushRegisteredException(); } - if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { + if (storyTypes.isEmpty() && !TextSecurePreferences.isReadReceiptsEnabled(context)) { Log.w(TAG, "Read receipts not enabled!"); return; } + if (messageIds.isEmpty()) { + Log.w(TAG, "No messages in this batch are allowed to be sent!"); + return; + } + if (messageSentTimestamps.isEmpty()) { Log.w(TAG, "No sync timestamps!"); return; diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt index 072e2351f..67228ebca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt @@ -44,16 +44,24 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) { * Marks whether the user has seen the beta dialog */ private const val USER_HAS_SEEN_BETA_DIALOG = "stories.user.has.seen.beta.dialog" + + /** + * Whether or not the user will send and receive viewed receipts for stories + */ + private const val STORY_VIEWED_RECEIPTS = "stories.viewed.receipts" } - override fun onFirstEverAppLaunch() = Unit + override fun onFirstEverAppLaunch() { + viewedReceiptsEnabled = true + } override fun getKeysToIncludeInBackup(): MutableList = mutableListOf( MANUAL_FEATURE_DISABLE, USER_HAS_ADDED_TO_A_STORY, USER_HAS_SEEN_FIRST_NAV_VIEW, HAS_DOWNLOADED_ONBOARDING_STORY, - USER_HAS_SEEN_BETA_DIALOG + USER_HAS_SEEN_BETA_DIALOG, + STORY_VIEWED_RECEIPTS ) var isFeatureDisabled: Boolean by booleanValue(MANUAL_FEATURE_DISABLE, false) @@ -70,6 +78,12 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) { var userHasSeenBetaDialog: Boolean by booleanValue(USER_HAS_SEEN_BETA_DIALOG, false) + var viewedReceiptsEnabled: Boolean by booleanValue(STORY_VIEWED_RECEIPTS, false) + + fun isViewedReceiptsStateSet(): Boolean { + return store.containsKey(STORY_VIEWED_RECEIPTS) + } + fun setLatestStorySend(storySend: StorySend) { synchronized(this) { val storySends: List = getList(LATEST_STORY_SENDS, StorySendSerializer) diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index 5a7c39631..d4aebca91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -100,6 +100,7 @@ import org.thoughtcrime.securesms.jobs.SenderKeyDistributionSendJob; import org.thoughtcrime.securesms.jobs.StickerPackDownloadJob; import org.thoughtcrime.securesms.jobs.TrimThreadJob; import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.keyvalue.StoryValues; import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; @@ -2536,18 +2537,28 @@ public final class MessageContentProcessor { @NonNull SignalServiceReceiptMessage message, @NonNull Recipient senderRecipient) { - if (!TextSecurePreferences.isReadReceiptsEnabled(context)) { + boolean readReceipts = TextSecurePreferences.isReadReceiptsEnabled(context); + boolean storyViewedReceipts = SignalStore.storyValues().getViewedReceiptsEnabled(); + + if (!readReceipts && !storyViewedReceipts) { log("Ignoring viewed receipts for IDs: " + Util.join(message.getTimestamps(), ", ")); return; } - log(TAG, "Processing viewed receipts. Sender: " + senderRecipient.getId() + ", Device: " + content.getSenderDevice() + ", Timestamps: " + Util.join(message.getTimestamps(), ", ")); + log(TAG, "Processing viewed receipts. Sender: " + senderRecipient.getId() + ", Device: " + content.getSenderDevice() + ", Only Stories: " + (!readReceipts && storyViewedReceipts) + ", Timestamps: " + Util.join(message.getTimestamps(), ", ")); List ids = Stream.of(message.getTimestamps()) .map(t -> new SyncMessageId(senderRecipient.getId(), t)) .toList(); - Collection unhandled = SignalDatabase.mmsSms().incrementViewedReceiptCounts(ids, content.getTimestamp()); + final Collection unhandled; + if (readReceipts && storyViewedReceipts) { + unhandled = SignalDatabase.mmsSms().incrementViewedReceiptCounts(ids, content.getTimestamp()); + } else if (readReceipts) { + unhandled = SignalDatabase.mmsSms().incrementViewedNonStoryReceiptCounts(ids, content.getTimestamp()); + } else { + unhandled = SignalDatabase.mmsSms().incrementViewedStoryReceiptCounts(ids, content.getTimestamp()); + } Set handled = new HashSet<>(ids); handled.removeAll(unhandled); diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 386c8bbf0..ce1201f7e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -109,9 +109,10 @@ public class ApplicationMigrations { static final int KBS_MIGRATION_2 = 65; static final int PNI_2 = 66; static final int SYSTEM_NAME_SYNC = 67; + static final int STORY_VIEWED_STATE = 68; } - public static final int CURRENT_VERSION = 67; + public static final int CURRENT_VERSION = 68; /** * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call @@ -481,6 +482,10 @@ public class ApplicationMigrations { jobs.put(Version.SYSTEM_NAME_SYNC, new StorageServiceSystemNameMigrationJob()); } + if (lastSeenVersion < Version.STORY_VIEWED_STATE) { + jobs.put(Version.STORY_VIEWED_STATE, new StoryViewedReceiptsStateMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/StoryViewedReceiptsStateMigrationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/migrations/StoryViewedReceiptsStateMigrationJob.kt new file mode 100644 index 000000000..ed98a00bc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/StoryViewedReceiptsStateMigrationJob.kt @@ -0,0 +1,42 @@ +package org.thoughtcrime.securesms.migrations + +import org.thoughtcrime.securesms.database.SignalDatabase.Companion.recipients +import org.thoughtcrime.securesms.jobmanager.Data +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.storage.StorageSyncHelper +import org.thoughtcrime.securesms.util.TextSecurePreferences + +/** + * Added as a way to initialize the story viewed receipts setting. + */ +internal class StoryViewedReceiptsStateMigrationJob( + parameters: Parameters = Parameters.Builder().build() +) : MigrationJob(parameters) { + companion object { + const val KEY = "StoryViewedReceiptsStateMigrationJob" + } + + override fun getFactoryKey(): String = KEY + + override fun isUiBlocking(): Boolean = false + + override fun performMigration() { + if (!SignalStore.storyValues().isViewedReceiptsStateSet()) { + SignalStore.storyValues().viewedReceiptsEnabled = TextSecurePreferences.isReadReceiptsEnabled(context) + if (SignalStore.account().isRegistered) { + recipients.markNeedsSync(Recipient.self().id) + StorageSyncHelper.scheduleSyncForDataChange() + } + } + } + + override fun shouldRetry(e: Exception): Boolean = false + + class Factory : Job.Factory { + override fun create(parameters: Parameters, data: Data): StoryViewedReceiptsStateMigrationJob { + return StoryViewedReceiptsStateMigrationJob(parameters) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.java index 01dcec58b..6f534fab1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.java @@ -12,6 +12,7 @@ import org.whispersystems.signalservice.api.storage.SignalAccountRecord; import org.whispersystems.signalservice.api.storage.SignalAccountRecord.PinnedConversation; import org.whispersystems.signalservice.api.util.OptionalUtil; import org.whispersystems.signalservice.internal.storage.protos.AccountRecord; +import org.whispersystems.signalservice.internal.storage.protos.OptionalBool; import java.util.Arrays; import java.util.List; @@ -92,6 +93,13 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor remoteIds, @NonNull Collection localIds) @@ -111,6 +112,9 @@ public final class StorageSyncHelper { .map(recipientDatabase::getRecordForSync) .toList(); + final OptionalBool storyViewReceiptsState = SignalStore.storyValues().getViewedReceiptsEnabled() ? OptionalBool.ENABLED + : OptionalBool.DISABLED; + if (self.getStorageServiceId() == null) { Log.w(TAG, "[buildAccountRecord] No storageId for self! Generating. (Record had ID: " + (record != null && record.getStorageId() != null) + ")"); SignalDatabase.recipients().updateStorageId(self.getId(), generateKey()); @@ -145,6 +149,7 @@ public final class StorageSyncHelper { .setHasSetMyStoriesPrivacy(SignalStore.storyValues().getUserHasBeenNotifiedAboutStories()) .setHasViewedOnboardingStory(SignalStore.storyValues().getUserHasSeenOnboardingStory()) .setStoriesDisabled(SignalStore.storyValues().isFeatureDisabled()) + .setStoryViewReceiptsState(storyViewReceiptsState) .build(); return SignalStorageRecord.forAccount(account); @@ -174,6 +179,12 @@ public final class StorageSyncHelper { SignalStore.storyValues().setUserHasSeenOnboardingStory(update.getNew().hasViewedOnboardingStory()); SignalStore.storyValues().setFeatureDisabled(update.getNew().isStoriesDisabled()); + if (update.getNew().getStoryViewReceiptsState() == OptionalBool.UNSET) { + SignalStore.storyValues().setViewedReceiptsEnabled(update.getNew().isReadReceiptsEnabled()); + } else { + SignalStore.storyValues().setViewedReceiptsEnabled(update.getNew().getStoryViewReceiptsState() == OptionalBool.ENABLED); + } + if (update.getNew().isSubscriptionManuallyCancelled()) { SignalStore.donationsValues().updateLocalStateForManualCancellation(); } else { @@ -233,7 +244,7 @@ public final class StorageSyncHelper { /** * @return True if there exist some keys that have matching raw ID's but different types, - * otherwise false. + * otherwise false. */ public boolean hasTypeMismatches() { return hasTypeMismatches; diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt index 86278ebf6..e1806af0c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/my/MyStoriesItem.kt @@ -16,11 +16,11 @@ import org.thoughtcrime.securesms.components.menu.SignalContextMenu import org.thoughtcrime.securesms.components.settings.PreferenceModel import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader import org.thoughtcrime.securesms.mms.GlideApp import org.thoughtcrime.securesms.stories.StoryTextPostModel import org.thoughtcrime.securesms.util.DateUtils -import org.thoughtcrime.securesms.util.TextSecurePreferences import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder @@ -110,7 +110,7 @@ object MyStoriesItem { presentDateOrStatus(model) if (model.distributionStory.messageRecord.isSent) { - if (TextSecurePreferences.isReadReceiptsEnabled(context)) { + if (SignalStore.storyValues().viewedReceiptsEnabled) { viewCount.text = context.resources.getQuantityString( R.plurals.MyStories__d_views, model.distributionStory.messageRecord.viewedReceiptCount, diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt index 1b9d3ca2f..c012b75b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsFragment.kt @@ -150,6 +150,19 @@ class StoriesPrivacySettingsFragment : configure { dividerPref() + sectionHeaderPref(R.string.StoriesPrivacySettingsFragment__story_views) + + switchPref( + title = DSLSettingsText.from(R.string.StoriesPrivacySettingsFragment__view_receipts), + summary = DSLSettingsText.from(R.string.StoriesPrivacySettingsFragment__see_and_share), + isChecked = state.areViewReceiptsEnabled, + onClick = { + viewModel.toggleViewReceipts() + } + ) + + dividerPref() + clickPref( title = DSLSettingsText.from(R.string.StoriesPrivacySettingsFragment__turn_off_stories), summary = DSLSettingsText.from( diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt index de72a5a4c..d5a5653b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsRepository.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.stories.settings.story import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Single import io.reactivex.rxjava3.schedulers.Schedulers +import org.signal.core.util.concurrent.SignalExecutors import org.thoughtcrime.securesms.database.GroupDatabase import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies @@ -36,6 +37,12 @@ class StoriesPrivacySettingsRepository { }.subscribeOn(Schedulers.io()) } + fun onSettingsChanged() { + SignalExecutors.BOUNDED_IO.execute { + Stories.onStorySettingsChanged(Recipient.self().id) + } + } + fun userHasOutgoingStories(): Single { return Single.fromCallable { SignalDatabase.mms.getAllOutgoingStories(false, -1).use { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsState.kt index 74107df97..d77b38a81 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsState.kt @@ -4,7 +4,8 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchData data class StoriesPrivacySettingsState( val areStoriesEnabled: Boolean, + val areViewReceiptsEnabled: Boolean, val isUpdatingEnabledState: Boolean = false, val storyContactItems: List = emptyList(), - val userHasStories: Boolean = false + val userHasStories: Boolean = false, ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt index f062e73e5..4d656b691 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/settings/story/StoriesPrivacySettingsViewModel.kt @@ -12,6 +12,7 @@ import org.signal.paging.ProxyPagingController import org.thoughtcrime.securesms.contacts.paged.ContactSearchConfiguration import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey import org.thoughtcrime.securesms.contacts.paged.ContactSearchPagedDataSource +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.util.rx.RxStore @@ -22,7 +23,8 @@ class StoriesPrivacySettingsViewModel : ViewModel() { private val store = RxStore( StoriesPrivacySettingsState( - areStoriesEnabled = Stories.isFeatureEnabled() + areStoriesEnabled = Stories.isFeatureEnabled(), + areViewReceiptsEnabled = SignalStore.storyValues().viewedReceiptsEnabled ) ) @@ -77,6 +79,12 @@ class StoriesPrivacySettingsViewModel : ViewModel() { } } + fun toggleViewReceipts() { + SignalStore.storyValues().viewedReceiptsEnabled = !SignalStore.storyValues().viewedReceiptsEnabled + store.update { it.copy(areViewReceiptsEnabled = SignalStore.storyValues().viewedReceiptsEnabled) } + repository.onSettingsChanged() + } + fun displayGroupsAsStories(recipientIds: List) { disposables += repository.markGroupsAsStories(recipientIds).subscribe { pagingController.onDataInvalidated() diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt index e896b1ccc..d8000db21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageRepository.kt @@ -26,7 +26,6 @@ import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.stories.Stories import org.thoughtcrime.securesms.util.Base64 -import org.thoughtcrime.securesms.util.TextSecurePreferences /** * Open for testing. @@ -39,7 +38,7 @@ open class StoryViewerPageRepository(context: Context) { private val context = context.applicationContext - fun isReadReceiptsEnabled(): Boolean = TextSecurePreferences.isReadReceiptsEnabled(context) + fun isReadReceiptsEnabled(): Boolean = SignalStore.storyValues().viewedReceiptsEnabled private fun getStoryRecords(recipientId: RecipientId, isOutgoingOnly: Boolean): Observable> { return Observable.create { emitter -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsRepository.kt index 9b032b2dd..94182642a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/views/StoryViewsRepository.kt @@ -9,8 +9,8 @@ import org.thoughtcrime.securesms.database.DatabaseObserver import org.thoughtcrime.securesms.database.GroupReceiptDatabase import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.util.TextSecurePreferences class StoryViewsRepository { @@ -18,7 +18,7 @@ class StoryViewsRepository { private val TAG = Log.tag(StoryViewsRepository::class.java) } - fun isReadReceiptsEnabled(): Boolean = TextSecurePreferences.isReadReceiptsEnabled(ApplicationDependencies.getApplication()) + fun isReadReceiptsEnabled(): Boolean = SignalStore.storyValues().viewedReceiptsEnabled fun getStoryRecipient(storyId: Long): Single { return Single.fromCallable { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index efb11f340..525214a5a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -5311,6 +5311,12 @@ Story privacy Stories + + Story views + + View receipts + + See and share when stories are viewed. If disabled, you won\'t see when others view your story. New story 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 3e4062a7e..a3b946d57 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 @@ -10,6 +10,7 @@ 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.internal.storage.protos.AccountRecord; +import org.whispersystems.signalservice.internal.storage.protos.OptionalBool; import java.util.ArrayList; import java.util.Arrays; @@ -182,6 +183,10 @@ public final class SignalAccountRecord implements SignalRecord { diff.add("StoriesDisabled"); } + if (getStoryViewReceiptsState() != that.getStoryViewReceiptsState()) { + diff.add("StoryViewedReceipts"); + } + return diff.toString(); } else { return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName(); @@ -300,6 +305,10 @@ public final class SignalAccountRecord implements SignalRecord { return proto.getStoriesDisabled(); } + public OptionalBool getStoryViewReceiptsState() { + return proto.getStoryViewReceiptsEnabled(); + } + public AccountRecord toProto() { return proto; } @@ -657,6 +666,11 @@ public final class SignalAccountRecord implements SignalRecord { return this; } + public Builder setStoryViewReceiptsState(OptionalBool storyViewedReceiptsEnabled) { + builder.setStoryViewReceiptsEnabled(storyViewedReceiptsEnabled); + return this; + } + private static AccountRecord.Builder parseUnknowns(byte[] serializedUnknowns) { try { return AccountRecord.parseFrom(serializedUnknowns).toBuilder(); diff --git a/libsignal/service/src/main/proto/StorageService.proto b/libsignal/service/src/main/proto/StorageService.proto index 3b47f0338..47efb131b 100644 --- a/libsignal/service/src/main/proto/StorageService.proto +++ b/libsignal/service/src/main/proto/StorageService.proto @@ -10,6 +10,12 @@ package signalservice; option java_package = "org.whispersystems.signalservice.internal.storage.protos"; option java_multiple_files = true; +enum OptionalBool { + UNSET = 0; + ENABLED = 1; + DISABLED = 2; +} + message StorageManifest { uint64 version = 1; bytes value = 2; @@ -176,6 +182,7 @@ message AccountRecord { bool hasViewedOnboardingStory = 27; reserved /* storiesDisabled */ 28; bool storiesDisabled = 29; + OptionalBool storyViewReceiptsEnabled = 30; } message StoryDistributionListRecord {