From 94bd3101c9b1d686d7e302454e131986c9f80e4d Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 19 Oct 2022 14:53:31 -0300 Subject: [PATCH] Add support for stories "seen" state. --- .../app/internal/InternalSettingsViewModel.kt | 2 +- .../securesms/database/MessageDatabase.java | 4 ++ .../securesms/database/MmsDatabase.java | 27 ++++++++++- .../securesms/database/SmsDatabase.java | 10 +++++ .../securesms/jobs/JobManagerFactories.java | 2 + .../securesms/keyvalue/StoryValues.kt | 20 +++++++-- .../migrations/ApplicationMigrations.java | 7 ++- .../migrations/StoryReadStateMigrationJob.kt | 45 +++++++++++++++++++ .../service/ExpiringStoriesManager.kt | 4 +- .../storage/AccountRecordProcessor.java | 12 +++-- .../securesms/storage/StorageSyncHelper.java | 9 ++-- .../stories/landing/StoriesLandingFragment.kt | 1 + .../landing/StoriesLandingRepository.kt | 22 +++++++++ .../landing/StoriesLandingViewModel.kt | 4 ++ .../viewer/page/StoryViewerPageRepository.kt | 2 +- .../api/storage/SignalAccountRecord.java | 13 ++++++ .../src/main/proto/StorageService.proto | 1 + 17 files changed, 168 insertions(+), 17 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/migrations/StoryReadStateMigrationJob.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt index 79ca353d3..87197fb09 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt @@ -129,7 +129,7 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito fun onClearOnboardingState() { SignalStore.storyValues().hasDownloadedOnboardingStory = false - SignalStore.storyValues().userHasSeenOnboardingStory = false + SignalStore.storyValues().userHasViewedOnboardingStory = false Stories.onStorySettingsChanged(Recipient.self().id) refresh() StoryOnboardingDownloadJob.enqueueIfNeeded() 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 56b0a0544..b0f4aceb4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -203,7 +203,11 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns, public abstract @NonNull Reader getOutgoingStoriesTo(@NonNull RecipientId recipientId); public abstract @NonNull Reader getAllOutgoingStories(boolean reverse, int limit); public abstract @NonNull Reader getAllOutgoingStoriesAt(long sentTimestamp); + public abstract @NonNull List markAllIncomingStoriesRead(); public abstract @NonNull List getOrderedStoryRecipientsAndIds(boolean isOutgoingOnly); + + public abstract void markOnboardingStoryRead(); + public abstract @NonNull Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit); public abstract @NonNull MessageId getStoryId(@NonNull RecipientId authorId, long sentTimestamp) throws NoSuchMessageException; public abstract int getNumberOfStoryReplies(long parentStoryId); 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 4b403bb5e..8feab7029 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -655,6 +655,31 @@ public class MmsDatabase extends MessageDatabase { return new Reader(cursor); } + @Override + public @NonNull List markAllIncomingStoriesRead() { + String where = IS_STORY_CLAUSE + " AND NOT (" + getOutgoingTypeClause() + ") AND " + READ + " = 0"; + + List 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 markedMessageInfos = setMessagesRead(where, SqlUtil.buildArgs(recipientId)); + if (!markedMessageInfos.isEmpty()) { + notifyConversationListListeners(); + } + } + @Override public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit) { long threadId = SignalDatabase.threads().getThreadIdIfExistsFor(recipientId); @@ -801,7 +826,7 @@ public class MmsDatabase extends MessageDatabase { + "FROM " + TABLE_NAME + "\n" + "JOIN " + ThreadDatabase.TABLE_NAME + "\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)) { if (cursor != null) { 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 00f857137..3a3577f60 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -1488,11 +1488,21 @@ public class SmsDatabase extends MessageDatabase { throw new UnsupportedOperationException(); } + @Override + public @NonNull List markAllIncomingStoriesRead() { + throw new UnsupportedOperationException(); + } + @Override public @NonNull List getOrderedStoryRecipientsAndIds(boolean isOutgoingOnly) { throw new UnsupportedOperationException(); } + @Override + public void markOnboardingStoryRead() { + throw new UnsupportedOperationException(); + } + @Override public @NonNull MessageDatabase.Reader getAllStoriesFor(@NonNull RecipientId recipientId, int limit) { throw new UnsupportedOperationException(); 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 2e90c6090..b8f5c8102 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.StoryReadStateMigrationJob; import org.thoughtcrime.securesms.migrations.StoryViewedReceiptsStateMigrationJob; import org.thoughtcrime.securesms.migrations.SyncDistributionListsMigrationJob; import org.thoughtcrime.securesms.migrations.TrimByLengthSettingsMigrationJob; @@ -226,6 +227,7 @@ public final class JobManagerFactories { put(StorageCapabilityMigrationJob.KEY, new StorageCapabilityMigrationJob.Factory()); put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory()); put(StorageServiceSystemNameMigrationJob.KEY, new StorageServiceSystemNameMigrationJob.Factory()); + put(StoryReadStateMigrationJob.KEY, new StoryReadStateMigrationJob.Factory()); put(StoryViewedReceiptsStateMigrationJob.KEY, new StoryViewedReceiptsStateMigrationJob.Factory()); put(TrimByLengthSettingsMigrationJob.KEY, new TrimByLengthSettingsMigrationJob.Factory()); put(UserNotificationMigrationJob.KEY, new UserNotificationMigrationJob.Factory()); 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 67228ebca..97214f428 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/StoryValues.kt @@ -36,9 +36,14 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) { 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 @@ -61,7 +66,8 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) { USER_HAS_SEEN_FIRST_NAV_VIEW, HAS_DOWNLOADED_ONBOARDING_STORY, 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) @@ -74,7 +80,9 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) { 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) @@ -84,6 +92,10 @@ internal class StoryValues(store: KeyValueStore) : SignalStoreValues(store) { return store.containsKey(STORY_VIEWED_RECEIPTS) } + fun hasUserOnboardingStoryReadBeenSet(): Boolean { + return store.containsKey(USER_HAS_READ_ONBOARDING_STORY) + } + fun setLatestStorySend(storySend: StorySend) { synchronized(this) { val storySends: List = getList(LATEST_STORY_SENDS, StorySendSerializer) 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 ce1201f7e..3c9bff36e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -110,9 +110,10 @@ public class ApplicationMigrations { static final int PNI_2 = 66; static final int SYSTEM_NAME_SYNC = 67; 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 @@ -486,6 +487,10 @@ public class ApplicationMigrations { jobs.put(Version.STORY_VIEWED_STATE, new StoryViewedReceiptsStateMigrationJob()); } + if (lastSeenVersion < Version.STORY_READ_STATE) { + jobs.put(Version.STORY_READ_STATE, new StoryReadStateMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/StoryReadStateMigrationJob.kt b/app/src/main/java/org/thoughtcrime/securesms/migrations/StoryReadStateMigrationJob.kt new file mode 100644 index 000000000..4c033bd71 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/StoryReadStateMigrationJob.kt @@ -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 { + override fun create(parameters: Parameters, data: Data): StoryReadStateMigrationJob { + return StoryReadStateMigrationJob(parameters) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringStoriesManager.kt b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringStoriesManager.kt index cd1729db5..a26c45d10 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringStoriesManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/ExpiringStoriesManager.kt @@ -32,7 +32,7 @@ class ExpiringStoriesManager( @WorkerThread 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 delay = (STORY_LIFESPAN - timeSinceSend).coerceAtLeast(0) @@ -44,7 +44,7 @@ class ExpiringStoriesManager( @WorkerThread override fun executeEvent(event: Event) { 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") } 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 6f534fab1..11f0e246f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/storage/AccountRecordProcessor.java @@ -123,8 +123,9 @@ public class AccountRecordProcessor extends DefaultStorageRecordProcessor = 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) + } + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingViewModel.kt index f2454a95d..be9387b78 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingViewModel.kt @@ -58,6 +58,10 @@ class StoriesLandingViewModel(private val storiesLandingRepository: StoriesLandi store.update { it.copy(searchQuery = query) } } + fun markStoriesRead() { + storiesLandingRepository.markStoriesRead() + } + class Factory(private val storiesLandingRepository: StoriesLandingRepository) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { return modelClass.cast(StoriesLandingViewModel(storiesLandingRepository)) as T 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 d8000db21..e999fff02 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 @@ -173,7 +173,7 @@ open class StoryViewerPageRepository(context: Context) { ApplicationDependencies.getDatabaseObserver().notifyConversationListListeners() if (storyPost.sender.isReleaseNotes) { - SignalStore.storyValues().userHasSeenOnboardingStory = true + SignalStore.storyValues().userHasViewedOnboardingStory = true Stories.onStorySettingsChanged(Recipient.self().id) } else { ApplicationDependencies.getJobManager().add( 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 a3b946d57..c03efa174 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 @@ -187,6 +187,10 @@ public final class SignalAccountRecord implements SignalRecord { diff.add("StoryViewedReceipts"); } + if (hasReadOnboardingStory() != that.hasReadOnboardingStory()) { + diff.add("HasReadOnboardingStory"); + } + return diff.toString(); } else { return "Different class. " + getClass().getSimpleName() + " | " + other.getClass().getSimpleName(); @@ -309,6 +313,10 @@ public final class SignalAccountRecord implements SignalRecord { return proto.getStoryViewReceiptsEnabled(); } + public boolean hasReadOnboardingStory() { + return proto.getHasReadOnboardingStory(); + } + public AccountRecord toProto() { return proto; } @@ -671,6 +679,11 @@ public final class SignalAccountRecord implements SignalRecord { return this; } + public Builder setHasReadOnboardingStory(boolean hasReadOnboardingStory) { + builder.setHasReadOnboardingStory(hasReadOnboardingStory); + 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 47efb131b..dcb99c4dc 100644 --- a/libsignal/service/src/main/proto/StorageService.proto +++ b/libsignal/service/src/main/proto/StorageService.proto @@ -183,6 +183,7 @@ message AccountRecord { reserved /* storiesDisabled */ 28; bool storiesDisabled = 29; OptionalBool storyViewReceiptsEnabled = 30; + bool hasReadOnboardingStory = 31; } message StoryDistributionListRecord {