Add support for stories "seen" state.

This commit is contained in:
Alex Hart 2022-10-19 14:53:31 -03:00 committed by Cody Henthorne
parent 995a4ad6ec
commit 94bd3101c9
17 changed files with 168 additions and 17 deletions

View file

@ -129,7 +129,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
fun onClearOnboardingState() { fun onClearOnboardingState() {
SignalStore.storyValues().hasDownloadedOnboardingStory = false SignalStore.storyValues().hasDownloadedOnboardingStory = false
SignalStore.storyValues().userHasSeenOnboardingStory = false SignalStore.storyValues().userHasViewedOnboardingStory = false
Stories.onStorySettingsChanged(Recipient.self().id) Stories.onStorySettingsChanged(Recipient.self().id)
refresh() refresh()
StoryOnboardingDownloadJob.enqueueIfNeeded() StoryOnboardingDownloadJob.enqueueIfNeeded()

View file

@ -203,7 +203,11 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns,
public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId); public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId);
public abstract @NonNull Reader getAllOutgoingStories(boolean reverse, int limit); public abstract @NonNull Reader getAllOutgoingStories(boolean reverse, int limit);
public abstract @NonNull Reader getAllOutgoingStoriesAt(long sentTimestamp); public abstract @NonNull Reader getAllOutgoingStoriesAt(long sentTimestamp);
public abstract @NonNull List<MarkedMessageInfo> markAllIncomingStoriesRead();
public abstract @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds(boolean isOutgoingOnly); public abstract @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds(boolean isOutgoingOnly);
public abstract void markOnboardingStoryRead();
public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit); public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit);
public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException; public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException;
public abstract int getNumberOfStoryReplies(long parentStoryId); public abstract int getNumberOfStoryReplies(long parentStoryId);

View file

@ -655,6 +655,31 @@ public class MmsDatabase extends MessageDatabase {
return new Reader(cursor); return new Reader(cursor);
} }
@Override
public @NonNull List<MarkedMessageInfo> markAllIncomingStoriesRead() {
String where = IS_STORY_CLAUSE + " AND NOT (" + getOutgoingTypeClause() + ") AND " + READ + " = 0";
List<MarkedMessageInfo> markedMessageInfos = setMessagesRead(where, null);
notifyConversationListListeners();
return markedMessageInfos;
}
@Override
public void markOnboardingStoryRead() {
RecipientId recipientId = SignalStore.releaseChannelValues().getReleaseChannelRecipientId();
if (recipientId == null) {
return;
}
String where = IS_STORY_CLAUSE + " AND NOT (" + getOutgoingTypeClause() + ") AND " + READ + " = 0 AND " + RECIPIENT_ID + " = ?";
List<MarkedMessageInfo> markedMessageInfos = setMessagesRead(where, SqlUtil.buildArgs(recipientId));
if (!markedMessageInfos.isEmpty()) {
notifyConversationListListeners();
}
}
@Override @Override
public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit) { public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit) {
long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId); long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId);
@ -801,7 +826,7 @@ public class MmsDatabase extends MessageDatabase {
+ "FROM " + TABLE_NAME + "\n" + "FROM " + TABLE_NAME + "\n"
+ "JOIN " + ThreadDatabase.TABLE_NAME + "\n" + "JOIN " + ThreadDatabase.TABLE_NAME + "\n"
+ "ON " + TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + "\n" + "ON " + TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + "\n"
+ "WHERE " + IS_STORY_CLAUSE + " AND (" + getOutgoingTypeClause() + ") = 0 AND " + VIEWED_RECEIPT_COUNT + " = 0"; + "WHERE " + IS_STORY_CLAUSE + " AND (" + getOutgoingTypeClause() + ") = 0 AND " + VIEWED_RECEIPT_COUNT + " = 0 AND " + TABLE_NAME + "." + READ + " = 0";
try (Cursor cursor = db.rawQuery(query, null)) { try (Cursor cursor = db.rawQuery(query, null)) {
if (cursor != null) { if (cursor != null) {

View file

@ -1488,11 +1488,21 @@ public class SmsDatabase extends MessageDatabase {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public @NonNull List<MarkedMessageInfo> markAllIncomingStoriesRead() {
throw new UnsupportedOperationException();
}
@Override @Override
public @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds(boolean isOutgoingOnly) { public @NonNull List<StoryResult> getOrderedStoryRecipientsAndIds(boolean isOutgoingOnly) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }
@Override
public void markOnboardingStoryRead() {
throw new UnsupportedOperationException();
}
@Override @Override
public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit) { public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit) {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();

View file

@ -62,6 +62,7 @@ import org.thoughtcrime.securesms.migrations.StickerMyDailyLifeMigrationJob;
import org.thoughtcrime.securesms.migrations.StorageCapabilityMigrationJob; import org.thoughtcrime.securesms.migrations.StorageCapabilityMigrationJob;
import org.thoughtcrime.securesms.migrations.StorageServiceMigrationJob; import org.thoughtcrime.securesms.migrations.StorageServiceMigrationJob;
import org.thoughtcrime.securesms.migrations.StorageServiceSystemNameMigrationJob; import org.thoughtcrime.securesms.migrations.StorageServiceSystemNameMigrationJob;
import org.thoughtcrime.securesms.migrations.StoryReadStateMigrationJob;
import org.thoughtcrime.securesms.migrations.StoryViewedReceiptsStateMigrationJob; import org.thoughtcrime.securesms.migrations.StoryViewedReceiptsStateMigrationJob;
import org.thoughtcrime.securesms.migrations.SyncDistributionListsMigrationJob; import org.thoughtcrime.securesms.migrations.SyncDistributionListsMigrationJob;
import org.thoughtcrime.securesms.migrations.TrimByLengthSettingsMigrationJob; import org.thoughtcrime.securesms.migrations.TrimByLengthSettingsMigrationJob;
@ -226,6 +227,7 @@ public final class JobManagerFactories {
put(StorageCapabilityMigrationJob.KEY, new StorageCapabilityMigrationJob.Factory()); put(StorageCapabilityMigrationJob.KEY, new StorageCapabilityMigrationJob.Factory());
put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory()); put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory());
put(StorageServiceSystemNameMigrationJob.KEY, new StorageServiceSystemNameMigrationJob.Factory()); put(StorageServiceSystemNameMigrationJob.KEY, new StorageServiceSystemNameMigrationJob.Factory());
put(StoryReadStateMigrationJob.KEY, new StoryReadStateMigrationJob.Factory());
put(StoryViewedReceiptsStateMigrationJob.KEY, new StoryViewedReceiptsStateMigrationJob.Factory()); put(StoryViewedReceiptsStateMigrationJob.KEY, new StoryViewedReceiptsStateMigrationJob.Factory());
put(TrimByLengthSettingsMigrationJob.KEY, new TrimByLengthSettingsMigrationJob.Factory()); put(TrimByLengthSettingsMigrationJob.KEY, new TrimByLengthSettingsMigrationJob.Factory());
put(UserNotificationMigrationJob.KEY, new UserNotificationMigrationJob.Factory()); put(UserNotificationMigrationJob.KEY, new UserNotificationMigrationJob.Factory());

View file

@ -36,9 +36,14 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
private const val HAS_DOWNLOADED_ONBOARDING_STORY = "stories.has.downloaded.onboarding" private const val HAS_DOWNLOADED_ONBOARDING_STORY = "stories.has.downloaded.onboarding"
/** /**
* Marks whether the user has seen the onboarding story * Marks whether the user has opened and viewed the onboarding story
*/ */
private const val USER_HAS_SEEN_ONBOARDING_STORY = "stories.user.has.seen.onboarding" private const val USER_HAS_VIEWED_ONBOARDING_STORY = "stories.user.has.seen.onboarding"
/**
* Marks whether the user has seen the onboarding story in the stories landing page
*/
private const val USER_HAS_READ_ONBOARDING_STORY = "stories.user.has.read.onboarding"
/** /**
* Marks whether the user has seen the beta dialog * Marks whether the user has seen the beta dialog
@ -61,7 +66,8 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
USER_HAS_SEEN_FIRST_NAV_VIEW, USER_HAS_SEEN_FIRST_NAV_VIEW,
HAS_DOWNLOADED_ONBOARDING_STORY, HAS_DOWNLOADED_ONBOARDING_STORY,
USER_HAS_SEEN_BETA_DIALOG, USER_HAS_SEEN_BETA_DIALOG,
STORY_VIEWED_RECEIPTS STORY_VIEWED_RECEIPTS,
USER_HAS_READ_ONBOARDING_STORY
) )
var isFeatureDisabled: Boolean by booleanValue(MANUAL_FEATURE_DISABLE, false) var isFeatureDisabled: Boolean by booleanValue(MANUAL_FEATURE_DISABLE, false)
@ -74,7 +80,9 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
var hasDownloadedOnboardingStory: Boolean by booleanValue(HAS_DOWNLOADED_ONBOARDING_STORY, false) var hasDownloadedOnboardingStory: Boolean by booleanValue(HAS_DOWNLOADED_ONBOARDING_STORY, false)
var userHasSeenOnboardingStory: Boolean by booleanValue(USER_HAS_SEEN_ONBOARDING_STORY, false) var userHasViewedOnboardingStory: Boolean by booleanValue(USER_HAS_VIEWED_ONBOARDING_STORY, false)
var userHasReadOnboardingStory: Boolean by booleanValue(USER_HAS_READ_ONBOARDING_STORY, false)
var userHasSeenBetaDialog: Boolean by booleanValue(USER_HAS_SEEN_BETA_DIALOG, false) var userHasSeenBetaDialog: Boolean by booleanValue(USER_HAS_SEEN_BETA_DIALOG, false)
@ -84,6 +92,10 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) {
return store.containsKey(STORY_VIEWED_RECEIPTS) return store.containsKey(STORY_VIEWED_RECEIPTS)
} }
fun hasUserOnboardingStoryReadBeenSet(): Boolean {
return store.containsKey(USER_HAS_READ_ONBOARDING_STORY)
}
fun setLatestStorySend(storySend: StorySend) { fun setLatestStorySend(storySend: StorySend) {
synchronized(this) { synchronized(this) {
val storySends: List<StorySend> = getList(LATEST_STORY_SENDS, StorySendSerializer) val storySends: List<StorySend> = getList(LATEST_STORY_SENDS, StorySendSerializer)

View file

@ -110,9 +110,10 @@ public class ApplicationMigrations {
static final int PNI_2 = 66; static final int PNI_2 = 66;
static final int SYSTEM_NAME_SYNC = 67; static final int SYSTEM_NAME_SYNC = 67;
static final int STORY_VIEWED_STATE = 68; static final int STORY_VIEWED_STATE = 68;
static final int STORY_READ_STATE = 69;
} }
public static final int CURRENT_VERSION = 68; public static final int CURRENT_VERSION = 69;
/** /**
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call * This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
@ -486,6 +487,10 @@ public class ApplicationMigrations {
jobs.put(Version.STORY_VIEWED_STATE, new StoryViewedReceiptsStateMigrationJob()); jobs.put(Version.STORY_VIEWED_STATE, new StoryViewedReceiptsStateMigrationJob());
} }
if (lastSeenVersion < Version.STORY_READ_STATE) {
jobs.put(Version.STORY_READ_STATE, new StoryReadStateMigrationJob());
}
return jobs; return jobs;
} }

View file

@ -0,0 +1,45 @@
package org.thoughtcrime.securesms.migrations
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.mms
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
/**
* Added to initialize whether the user has seen the onboarding story
*/
internal class StoryReadStateMigrationJob(
parameters: Parameters = Parameters.Builder().build()
) : MigrationJob(parameters) {
companion object {
const val KEY = "StoryReadStateMigrationJob"
}
override fun getFactoryKey(): String = KEY
override fun isUiBlocking(): Boolean = false
override fun performMigration() {
if (!SignalStore.storyValues().hasUserOnboardingStoryReadBeenSet()) {
SignalStore.storyValues().userHasReadOnboardingStory = SignalStore.storyValues().userHasReadOnboardingStory
mms.markOnboardingStoryRead()
if (SignalStore.account().isRegistered) {
recipients.markNeedsSync(Recipient.self().id)
StorageSyncHelper.scheduleSyncForDataChange()
}
}
}
override fun shouldRetry(e: Exception): Boolean = false
class Factory : Job.Factory<StoryReadStateMigrationJob> {
override fun create(parameters: Parameters, data: Data): StoryReadStateMigrationJob {
return StoryReadStateMigrationJob(parameters)
}
}
}

View file

@ -32,7 +32,7 @@ class ExpiringStoriesManager(
@WorkerThread @WorkerThread
override fun getNextClosestEvent(): Event? { override fun getNextClosestEvent(): Event? {
val oldestTimestamp = mmsDatabase.getOldestStorySendTimestamp(SignalStore.storyValues().userHasSeenOnboardingStory) ?: return null val oldestTimestamp = mmsDatabase.getOldestStorySendTimestamp(SignalStore.storyValues().userHasViewedOnboardingStory) ?: return null
val timeSinceSend = System.currentTimeMillis() - oldestTimestamp val timeSinceSend = System.currentTimeMillis() - oldestTimestamp
val delay = (STORY_LIFESPAN - timeSinceSend).coerceAtLeast(0) val delay = (STORY_LIFESPAN - timeSinceSend).coerceAtLeast(0)
@ -44,7 +44,7 @@ class ExpiringStoriesManager(
@WorkerThread @WorkerThread
override fun executeEvent(event: Event) { override fun executeEvent(event: Event) {
val threshold = System.currentTimeMillis() - STORY_LIFESPAN val threshold = System.currentTimeMillis() - STORY_LIFESPAN
val deletes = mmsDatabase.deleteStoriesOlderThan(threshold, SignalStore.storyValues().userHasSeenOnboardingStory) val deletes = mmsDatabase.deleteStoriesOlderThan(threshold, SignalStore.storyValues().userHasViewedOnboardingStory)
Log.i(TAG, "Deleted $deletes stories before $threshold") Log.i(TAG, "Deleted $deletes stories before $threshold")
} }

View file

@ -123,8 +123,9 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
boolean hasSetMyStoriesPrivacy = remote.hasSetMyStoriesPrivacy(); boolean hasSetMyStoriesPrivacy = remote.hasSetMyStoriesPrivacy();
boolean hasViewedOnboardingStory = remote.hasViewedOnboardingStory(); boolean hasViewedOnboardingStory = remote.hasViewedOnboardingStory();
boolean storiesDisabled = remote.isStoriesDisabled(); 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, storyViewReceiptsState); boolean hasReadOnboardingStory = remote.hasReadOnboardingStory() || remote.hasViewedOnboardingStory() || local.hasReadOnboardingStory() || local.hasViewedOnboardingStory() ;
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); 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, hasReadOnboardingStory);
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, hasReadOnboardingStory);
if (matchesRemote) { if (matchesRemote) {
return remote; return remote;
@ -159,6 +160,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
.setHasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy) .setHasSetMyStoriesPrivacy(hasSetMyStoriesPrivacy)
.setHasViewedOnboardingStory(hasViewedOnboardingStory) .setHasViewedOnboardingStory(hasViewedOnboardingStory)
.setStoriesDisabled(storiesDisabled) .setStoriesDisabled(storiesDisabled)
.setHasReadOnboardingStory(hasReadOnboardingStory)
.build(); .build();
} }
} }
@ -206,7 +208,8 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
boolean hasSetMyStoriesPrivacy, boolean hasSetMyStoriesPrivacy,
boolean hasViewedOnboardingStory, boolean hasViewedOnboardingStory,
boolean storiesDisabled, boolean storiesDisabled,
@NonNull OptionalBool storyViewReceiptsState) @NonNull OptionalBool storyViewReceiptsState,
boolean hasReadOnboardingStory)
{ {
return Arrays.equals(contact.serializeUnknownFields(), unknownFields) && return Arrays.equals(contact.serializeUnknownFields(), unknownFields) &&
Objects.equals(contact.getGivenName().orElse(""), givenName) && Objects.equals(contact.getGivenName().orElse(""), givenName) &&
@ -235,6 +238,7 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor<Signal
contact.hasSetMyStoriesPrivacy() == hasSetMyStoriesPrivacy && contact.hasSetMyStoriesPrivacy() == hasSetMyStoriesPrivacy &&
contact.hasViewedOnboardingStory() == hasViewedOnboardingStory && contact.hasViewedOnboardingStory() == hasViewedOnboardingStory &&
contact.isStoriesDisabled() == storiesDisabled && contact.isStoriesDisabled() == storiesDisabled &&
contact.getStoryViewReceiptsState().equals(storyViewReceiptsState); contact.getStoryViewReceiptsState().equals(storyViewReceiptsState) &&
contact.hasReadOnboardingStory() == hasReadOnboardingStory;
} }
} }

View file

@ -31,7 +31,6 @@ import org.whispersystems.signalservice.api.storage.SignalStorageManifest;
import org.whispersystems.signalservice.api.storage.SignalStorageRecord; import org.whispersystems.signalservice.api.storage.SignalStorageRecord;
import org.whispersystems.signalservice.api.storage.StorageId; import org.whispersystems.signalservice.api.storage.StorageId;
import org.whispersystems.signalservice.api.util.OptionalUtil; import org.whispersystems.signalservice.api.util.OptionalUtil;
import org.whispersystems.signalservice.internal.storage.protos.AccountRecord;
import org.whispersystems.signalservice.internal.storage.protos.OptionalBool; import org.whispersystems.signalservice.internal.storage.protos.OptionalBool;
import java.util.Collection; import java.util.Collection;
@ -122,6 +121,8 @@ public final class StorageSyncHelper {
record = recipientDatabase.getRecordForSync(self.getId()); record = recipientDatabase.getRecordForSync(self.getId());
} }
final boolean hasReadOnboardingStory = SignalStore.storyValues().getUserHasViewedOnboardingStory() || SignalStore.storyValues().getUserHasReadOnboardingStory();
SignalAccountRecord account = new SignalAccountRecord.Builder(self.getStorageServiceId(), record != null ? record.getSyncExtras().getStorageProto() : null) SignalAccountRecord account = new SignalAccountRecord.Builder(self.getStorageServiceId(), record != null ? record.getSyncExtras().getStorageProto() : null)
.setProfileKey(self.getProfileKey()) .setProfileKey(self.getProfileKey())
.setGivenName(self.getProfileName().getGivenName()) .setGivenName(self.getProfileName().getGivenName())
@ -147,9 +148,10 @@ public final class StorageSyncHelper {
.setSubscriptionManuallyCancelled(SignalStore.donationsValues().isUserManuallyCancelled()) .setSubscriptionManuallyCancelled(SignalStore.donationsValues().isUserManuallyCancelled())
.setKeepMutedChatsArchived(SignalStore.settings().shouldKeepMutedChatsArchived()) .setKeepMutedChatsArchived(SignalStore.settings().shouldKeepMutedChatsArchived())
.setHasSetMyStoriesPrivacy(SignalStore.storyValues().getUserHasBeenNotifiedAboutStories()) .setHasSetMyStoriesPrivacy(SignalStore.storyValues().getUserHasBeenNotifiedAboutStories())
.setHasViewedOnboardingStory(SignalStore.storyValues().getUserHasSeenOnboardingStory()) .setHasViewedOnboardingStory(SignalStore.storyValues().getUserHasViewedOnboardingStory())
.setStoriesDisabled(SignalStore.storyValues().isFeatureDisabled()) .setStoriesDisabled(SignalStore.storyValues().isFeatureDisabled())
.setStoryViewReceiptsState(storyViewReceiptsState) .setStoryViewReceiptsState(storyViewReceiptsState)
.setHasReadOnboardingStory(hasReadOnboardingStory)
.build(); .build();
return SignalStorageRecord.forAccount(account); return SignalStorageRecord.forAccount(account);
@ -176,8 +178,9 @@ public final class StorageSyncHelper {
SignalStore.donationsValues().setDisplayBadgesOnProfile(update.getNew().isDisplayBadgesOnProfile()); SignalStore.donationsValues().setDisplayBadgesOnProfile(update.getNew().isDisplayBadgesOnProfile());
SignalStore.settings().setKeepMutedChatsArchived(update.getNew().isKeepMutedChatsArchived()); SignalStore.settings().setKeepMutedChatsArchived(update.getNew().isKeepMutedChatsArchived());
SignalStore.storyValues().setUserHasBeenNotifiedAboutStories(update.getNew().hasSetMyStoriesPrivacy()); SignalStore.storyValues().setUserHasBeenNotifiedAboutStories(update.getNew().hasSetMyStoriesPrivacy());
SignalStore.storyValues().setUserHasSeenOnboardingStory(update.getNew().hasViewedOnboardingStory()); SignalStore.storyValues().setUserHasViewedOnboardingStory(update.getNew().hasViewedOnboardingStory());
SignalStore.storyValues().setFeatureDisabled(update.getNew().isStoriesDisabled()); SignalStore.storyValues().setFeatureDisabled(update.getNew().isStoriesDisabled());
SignalStore.storyValues().setUserHasReadOnboardingStory(update.getNew().hasReadOnboardingStory());
if (update.getNew().getStoryViewReceiptsState() == OptionalBool.UNSET) { if (update.getNew().getStoryViewReceiptsState() == OptionalBool.UNSET) {
SignalStore.storyValues().setViewedReceiptsEnabled(update.getNew().isReadReceiptsEnabled()); SignalStore.storyValues().setViewedReceiptsEnabled(update.getNew().isReadReceiptsEnabled());

View file

@ -91,6 +91,7 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l
super.onResume() super.onResume()
viewModel.isTransitioningToAnotherScreen = false viewModel.isTransitioningToAnotherScreen = false
initializeSearchAction() initializeSearchAction()
viewModel.markStoriesRead()
} }
override fun onPause() { override fun onPause() {

View file

@ -4,18 +4,23 @@ import android.content.Context
import io.reactivex.rxjava3.core.Completable import io.reactivex.rxjava3.core.Completable
import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.core.Observable
import io.reactivex.rxjava3.schedulers.Schedulers import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.conversation.ConversationMessage import org.thoughtcrime.securesms.conversation.ConversationMessage
import org.thoughtcrime.securesms.database.DatabaseObserver import org.thoughtcrime.securesms.database.DatabaseObserver
import org.thoughtcrime.securesms.database.MessageDatabase
import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.DistributionListId import org.thoughtcrime.securesms.database.model.DistributionListId
import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.StoryResult import org.thoughtcrime.securesms.database.model.StoryResult
import org.thoughtcrime.securesms.database.model.StoryViewState import org.thoughtcrime.securesms.database.model.StoryViewState
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
import org.thoughtcrime.securesms.jobs.MultiDeviceReadUpdateJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.sms.MessageSender import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.stories.Stories
class StoriesLandingRepository(context: Context) { class StoriesLandingRepository(context: Context) {
@ -159,4 +164,21 @@ class StoriesLandingRepository(context: Context) {
SignalDatabase.recipients.setHideStory(recipientId, hideStory) SignalDatabase.recipients.setHideStory(recipientId, hideStory)
}.subscribeOn(Schedulers.io()) }.subscribeOn(Schedulers.io())
} }
/**
* Marks all stories as "seen" by the user (marking them as read in the database)
*/
fun markStoriesRead() {
SignalExecutors.BOUNDED_IO.execute {
val messageInfos: List<MessageDatabase.MarkedMessageInfo> = SignalDatabase.mms.markAllIncomingStoriesRead()
val releaseThread: Long? = SignalStore.releaseChannelValues().releaseChannelRecipientId?.let { SignalDatabase.threads.getThreadIdIfExistsFor(it) }
MultiDeviceReadUpdateJob.enqueue(messageInfos.filter { it.threadId == releaseThread }.map { it.syncMessageId })
if (messageInfos.any { it.threadId == releaseThread }) {
SignalStore.storyValues().userHasReadOnboardingStory = true
Stories.onStorySettingsChanged(Recipient.self().id)
}
}
}
} }

View file

@ -58,6 +58,10 @@ class StoriesLandingViewModel(private val storiesLandingRepository: StoriesLandi
store.update { it.copy(searchQuery = query) } store.update { it.copy(searchQuery = query) }
} }
fun markStoriesRead() {
storiesLandingRepository.markStoriesRead()
}
class Factory(private val storiesLandingRepository: StoriesLandingRepository) : ViewModelProvider.Factory { class Factory(private val storiesLandingRepository: StoriesLandingRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T { override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.cast(StoriesLandingViewModel(storiesLandingRepository)) as T return modelClass.cast(StoriesLandingViewModel(storiesLandingRepository)) as T

View file

@ -173,7 +173,7 @@ open class StoryViewerPageRepository(context: Context) {
ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners() ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners()
if (storyPost.sender.isReleaseNotes) { if (storyPost.sender.isReleaseNotes) {
SignalStore.storyValues().userHasSeenOnboardingStory = true SignalStore.storyValues().userHasViewedOnboardingStory = true
Stories.onStorySettingsChanged(Recipient.self().id) Stories.onStorySettingsChanged(Recipient.self().id)
} else { } else {
ApplicationDependencies.getJobManager().add( ApplicationDependencies.getJobManager().add(

View file

@ -187,6 +187,10 @@ public final class SignalAccountRecord implements SignalRecord {
diff.add("StoryViewedReceipts"); diff.add("StoryViewedReceipts");
} }
if (hasReadOnboardingStory() != that.hasReadOnboardingStory()) {
diff.add("HasReadOnboardingStory");
}
return diff.toString(); return diff.toString();
} else { } else {
return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName(); return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName();
@ -309,6 +313,10 @@ public final class SignalAccountRecord implements SignalRecord {
return proto.getStoryViewReceiptsEnabled(); return proto.getStoryViewReceiptsEnabled();
} }
public boolean hasReadOnboardingStory() {
return proto.getHasReadOnboardingStory();
}
public AccountRecord toProto() { public AccountRecord toProto() {
return proto; return proto;
} }
@ -671,6 +679,11 @@ public final class SignalAccountRecord implements SignalRecord {
return this; return this;
} }
public Builder setHasReadOnboardingStory(boolean hasReadOnboardingStory) {
builder.setHasReadOnboardingStory(hasReadOnboardingStory);
return this;
}
private static AccountRecord.Builder parseUnknowns(byte[] serializedUnknowns) { private static AccountRecord.Builder parseUnknowns(byte[] serializedUnknowns) {
try { try {
return AccountRecord.parseFrom(serializedUnknowns).toBuilder(); return AccountRecord.parseFrom(serializedUnknowns).toBuilder();

View file

@ -183,6 +183,7 @@ message AccountRecord {
reserved /* storiesDisabled */ 28; reserved /* storiesDisabled */ 28;
bool storiesDisabled = 29; bool storiesDisabled = 29;
OptionalBool storyViewReceiptsEnabled = 30; OptionalBool storyViewReceiptsEnabled = 30;
bool hasReadOnboardingStory = 31;
} }
message StoryDistributionListRecord { message StoryDistributionListRecord {