Add support for separate story view receipt control.

This reverts commit 1046265d23.
fork-5.53.8
Alex Hart 2022-10-13 12:46:13 -03:00 zatwierdzone przez Cody Henthorne
rodzic 2f2711c9a3
commit ca36eaacce
23 zmienionych plików z 332 dodań i 48 usunięć

Wyświetl plik

@ -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<MessageUpdate> incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, boolean storiesOnly);
public abstract Set<MessageUpdate> incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, @NonNull MessageQualifier messageType);
public abstract List<MarkedMessageInfo> setEntireThreadRead(long threadId);
public abstract List<MarkedMessageInfo> 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<MarkedMessageInfo> setGroupStoryMessagesReadSince(long threadId, long groupStoryId, long sinceTimestamp);
public abstract @NonNull List<StoryType> getStoryTypes(@NonNull List<MessageId> messageIds);
public abstract @NonNull StoryViewState getStoryViewState(@NonNull RecipientId recipientId);
public abstract void updateViewedStories(@NonNull Set<SyncMessageId> 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
}
}

Wyświetl plik

@ -379,7 +379,7 @@ public class MmsDatabase extends MessageDatabase {
@Override
public @NonNull List<MarkedMessageInfo> 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<MarkedMessageInfo> 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<MarkedMessageInfo>
setOutgoingGiftsRevealed(@NonNull List<Long> 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<MarkedMessageInfo> 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<MessageUpdate> incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, boolean storiesOnly) {
public Set<MessageUpdate> incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, MessageQualifier messageQualifier) {
SQLiteDatabase database = databaseHelper.getSignalWritableDatabase();
Set<MessageUpdate> 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<StoryType> getStoryTypes(@NonNull List<MessageId> messageIds) {
List<Long> 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<SqlUtil.Query> queries = SqlUtil.buildCollectionQuery(ID, mmsMessages);
HashMap<Long, StoryType> 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<MarkedMessageInfo> 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));

Wyświetl plik

@ -493,6 +493,10 @@ public class MmsSmsDatabase extends Database {
return incrementReceiptCounts(syncMessageIds, timestamp, MessageDatabase.ReceiptType.VIEWED);
}
public @NonNull Collection<SyncMessageId> incrementViewedNonStoryReceiptCounts(@NonNull List<SyncMessageId> 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<MessageUpdate> updates = incrementStoryReceiptCountInternal(id, timestamp, MessageDatabase.ReceiptType.VIEWED);
Set<MessageUpdate> 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<MessageUpdate> 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<SyncMessageId> incrementReceiptCounts(@NonNull List<SyncMessageId> syncMessageIds, long timestamp, @NonNull MessageDatabase.ReceiptType receiptType) {
return incrementReceiptCounts(syncMessageIds, timestamp, receiptType, MessageDatabase.MessageQualifier.ALL);
}
private @NonNull Collection<SyncMessageId> incrementReceiptCounts(@NonNull List<SyncMessageId> syncMessageIds, long timestamp, @NonNull MessageDatabase.ReceiptType receiptType, @NonNull MessageDatabase.MessageQualifier messageQualifier) {
SQLiteDatabase db = databaseHelper.getSignalWritableDatabase();
ThreadDatabase threadDatabase = SignalDatabase.threads();
Set<MessageUpdate> messageUpdates = new HashSet<>();
@ -575,7 +587,7 @@ public class MmsSmsDatabase extends Database {
db.beginTransaction();
try {
for (SyncMessageId id : syncMessageIds) {
Set<MessageUpdate> updates = incrementReceiptCountInternal(id, timestamp, receiptType);
Set<MessageUpdate> 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<MessageUpdate> incrementReceiptCountInternal(SyncMessageId syncMessageId, long timestamp, MessageDatabase.ReceiptType receiptType) {
private @NonNull Set<MessageUpdate> incrementReceiptCountInternal(SyncMessageId syncMessageId, long timestamp, MessageDatabase.ReceiptType receiptType, @NonNull MessageDatabase.MessageQualifier messageQualifier) {
Set<MessageUpdate> 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<MessageUpdate> incrementStoryReceiptCountInternal(@NonNull SyncMessageId syncMessageId, long timestamp, @NonNull MessageDatabase.ReceiptType receiptType) {
return SignalDatabase.mms().incrementReceiptCount(syncMessageId, timestamp, receiptType, true);
}
public void updateViewedStories(@NonNull Set<SyncMessageId> syncMessageIds) {
SignalDatabase.mms().updateViewedStories(syncMessageIds);
}

Wyświetl plik

@ -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<MessageUpdate> incrementReceiptCount(SyncMessageId messageId, long timestamp, @NonNull ReceiptType receiptType, boolean storiesOnly) {
public @NonNull Set<MessageUpdate> 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<StoryType> getStoryTypes(@NonNull List<MessageId> messageIds) {
throw new UnsupportedOperationException();
}
@Override
public void deleteGroupStoryReplies(long parentStoryId) {
throw new UnsupportedOperationException();

Wyświetl plik

@ -120,6 +120,18 @@ public class Data {
return integerArrays.get(key);
}
public List<Integer> getIntegerArrayAsList(@NonNull String key) {
throwIfAbsent(integerArrays, key);
int[] array = Objects.requireNonNull(integerArrays.get(key));
List<Integer> 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<Integer> 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;

Wyświetl plik

@ -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());

Wyświetl plik

@ -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<Long> messageSentTimestamps, @NonNull List<MessageId> 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<MessageId> messageIds = new LinkedList<>();
List<Long> messageSentTimestamps = new LinkedList<>();
List<StoryType> 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;

Wyświetl plik

@ -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<String> = 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<StorySend> = getList(LATEST_STORY_SENDS, StorySendSerializer)

Wyświetl plik

@ -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<SyncMessageId> ids = Stream.of(message.getTimestamps())
.map(t -> new SyncMessageId(senderRecipient.getId(), t))
.toList();
Collection<SyncMessageId> unhandled = SignalDatabase.mmsSms().incrementViewedReceiptCounts(ids, content.getTimestamp());
final Collection<SyncMessageId> 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<SyncMessageId> handled = new HashSet<>(ids);
handled.removeAll(unhandled);

Wyświetl plik

@ -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;
}

Wyświetl plik

@ -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<StoryViewedReceiptsStateMigrationJob> {
override fun create(parameters: Parameters, data: Data): StoryViewedReceiptsStateMigrationJob {
return StoryViewedReceiptsStateMigrationJob(parameters)
}
}
}

Wyświetl plik

@ -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<Signal
subscriber = local.getSubscriber();
}
OptionalBool storyViewReceiptsState;
if (remote.getStoryViewReceiptsState() == OptionalBool.UNSET) {
storyViewReceiptsState = local.getStoryViewReceiptsState();
} else {
storyViewReceiptsState = remote.getStoryViewReceiptsState();
}
byte[] unknownFields = remote.serializeUnknownFields();
String avatarUrlPath = OptionalUtil.or(remote.getAvatarUrlPath(), local.getAvatarUrlPath()).orElse("");
byte[] profileKey = OptionalUtil.or(remote.getProfileKey(), local.getProfileKey()).orElse(null);
@ -115,8 +123,8 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
boolean hasSetMyStoriesPrivacy = remote.hasSetMyStoriesPrivacy();
boolean hasViewedOnboardingStory = remote.hasViewedOnboardingStory();
boolean storiesDisabled = remote.isStoriesDisabled();
boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, universalExpireTimer, primarySendsSms, e164, defaultReactions, subscriber, displayBadgesOnProfile, subscriptionManuallyCancelled, keepMutedChatsArchived, hasSetMyStoriesPrivacy, hasViewedOnboardingStory, storiesDisabled);
boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, universalExpireTimer, primarySendsSms, e164, defaultReactions, subscriber, displayBadgesOnProfile, subscriptionManuallyCancelled, keepMutedChatsArchived, hasSetMyStoriesPrivacy, hasViewedOnboardingStory, storiesDisabled);
boolean matchesRemote = doParamsMatch(remote, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, universalExpireTimer, primarySendsSms, e164, defaultReactions, subscriber, displayBadgesOnProfile, subscriptionManuallyCancelled, keepMutedChatsArchived, hasSetMyStoriesPrivacy, hasViewedOnboardingStory, storiesDisabled, storyViewReceiptsState);
boolean matchesLocal = doParamsMatch(local, unknownFields, givenName, familyName, avatarUrlPath, profileKey, noteToSelfArchived, noteToSelfForcedUnread, readReceipts, typingIndicators, sealedSenderIndicators, linkPreviews, phoneNumberSharingMode, unlisted, pinnedConversations, preferContactAvatars, payments, universalExpireTimer, primarySendsSms, e164, defaultReactions, subscriber, displayBadgesOnProfile, subscriptionManuallyCancelled, keepMutedChatsArchived, hasSetMyStoriesPrivacy, hasViewedOnboardingStory, storiesDisabled, storyViewReceiptsState);
if (matchesRemote) {
return remote;
@ -197,7 +205,8 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
boolean keepMutedChatsArchived,
boolean hasSetMyStoriesPrivacy,
boolean hasViewedOnboardingStory,
boolean storiesDisabled)
boolean storiesDisabled,
@NonNull OptionalBool storyViewReceiptsState)
{
return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
Objects.equals(contact.getGivenName().orElse(""), givenName) &&
@ -225,6 +234,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
contact.isKeepMutedChatsArchived() == keepMutedChatsArchived &&
contact.hasSetMyStoriesPrivacy() == hasSetMyStoriesPrivacy &&
contact.hasViewedOnboardingStory() == hasViewedOnboardingStory &&
contact.isStoriesDisabled() == storiesDisabled;
contact.isStoriesDisabled() == storiesDisabled &&
contact.getStoryViewReceiptsState().equals(storyViewReceiptsState);
}
}

Wyświetl plik

@ -31,6 +31,8 @@ import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId;
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.Collection;
import java.util.List;
@ -56,10 +58,9 @@ public final class StorageSyncHelper {
* you which keys are exclusively remote and which are exclusively local.
*
* @param remoteIds All remote keys available.
* @param localIds All local keys available.
*
* @param localIds All local keys available.
* @return An object describing which keys are exclusive to the remote data set and which keys are
* exclusive to the local data set.
* exclusive to the local data set.
*/
public static @NonNull IdDifferenceResult findIdDifference(@NonNull Collection<StorageId> remoteIds,
@NonNull Collection<StorageId> 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;

Wyświetl plik

@ -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,

Wyświetl plik

@ -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(

Wyświetl plik

@ -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<Boolean> {
return Single.fromCallable {
SignalDatabase.mms.getAllOutgoingStories(false, -1).use {

Wyświetl plik

@ -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<ContactSearchData> = emptyList(),
val userHasStories: Boolean = false
val userHasStories: Boolean = false,
)

Wyświetl plik

@ -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<RecipientId>) {
disposables += repository.markGroupsAsStories(recipientIds).subscribe {
pagingController.onDataInvalidated()

Wyświetl plik

@ -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<List<MessageRecord>> {
return Observable.create { emitter ->

Wyświetl plik

@ -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<Recipient> {
return Single.fromCallable {

Wyświetl plik

@ -5311,6 +5311,12 @@
<string name="StoriesPrivacySettingsFragment__story_privacy">Story privacy</string>
<!-- Header for section that lists out stories -->
<string name="StoriesPrivacySettingsFragment__stories">Stories</string>
<!-- Story views header -->
<string name="StoriesPrivacySettingsFragment__story_views">Story views</string>
<!-- Story view receipts toggle title -->
<string name="StoriesPrivacySettingsFragment__view_receipts">View receipts</string>
<!-- Story view receipts toggle message -->
<string name="StoriesPrivacySettingsFragment__see_and_share">See and share when stories are viewed. If disabled, you won\'t see when others view your story.</string>
<!-- NewStoryItem -->
<string name="NewStoryItem__new_story">New story</string>

Wyświetl plik

@ -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();

Wyświetl plik

@ -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 {