From 5d4d6db1972dbfef8ed2343852c4385dd1f078e2 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Fri, 28 Oct 2022 16:51:13 -0300 Subject: [PATCH] Fix story viewed state retention. --- .../stories/viewer/StoryViewerActivity.kt | 16 +++++ .../viewer/page/StoryViewStateCache.kt | 67 +++++++++++++++++++ .../viewer/page/StoryViewStateViewModel.kt | 7 ++ .../viewer/page/StoryViewerPageFragment.kt | 5 +- .../viewer/page/StoryViewerPageRepository.kt | 4 +- 5 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewStateCache.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewStateViewModel.kt diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerActivity.kt index a8aa7c1138..20b8b76472 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/StoryViewerActivity.kt @@ -17,6 +17,8 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaControllerOwner import org.thoughtcrime.securesms.stories.StoryViewerArgs +import org.thoughtcrime.securesms.stories.viewer.page.StoryViewStateCache +import org.thoughtcrime.securesms.stories.viewer.page.StoryViewStateViewModel import org.thoughtcrime.securesms.util.FullscreenHelper import org.thoughtcrime.securesms.util.ServiceUtil import org.thoughtcrime.securesms.util.ViewUtil @@ -26,6 +28,7 @@ import kotlin.math.min class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner { private val viewModel: StoryVolumeViewModel by viewModels() + private val storyViewStateViewModel: StoryViewStateViewModel by viewModels() override lateinit var voiceNoteMediaController: VoiceNoteMediaController @@ -35,6 +38,13 @@ class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControll } override fun onCreate(savedInstanceState: Bundle?, ready: Boolean) { + if (savedInstanceState != null) { + val cache: StoryViewStateCache? = savedInstanceState.getParcelable(DATA_CACHE) + if (cache != null) { + storyViewStateViewModel.storyViewStateCache.putAll(cache) + } + } + StoryMutePolicy.initialize() Glide.get(this).setMemoryCategory(MemoryCategory.HIGH) FullscreenHelper.showSystemUI(window) @@ -59,6 +69,11 @@ class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControll } } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putParcelable(DATA_CACHE, storyViewStateViewModel.storyViewStateCache) + } + override fun onDestroy() { super.onDestroy() Glide.get(this).setMemoryCategory(MemoryCategory.NORMAL) @@ -115,6 +130,7 @@ class StoryViewerActivity : PassphraseRequiredActivity(), VoiceNoteMediaControll companion object { private const val ARGS = "story.viewer.args" + private const val DATA_CACHE = "story.viewer.cache" @JvmStatic fun createIntent( diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewStateCache.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewStateCache.kt new file mode 100644 index 0000000000..34c4e7dd96 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewStateCache.kt @@ -0,0 +1,67 @@ +package org.thoughtcrime.securesms.stories.viewer.page + +import android.os.Parcel +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import org.thoughtcrime.securesms.util.ParcelUtil + +/** + * Activity-bounds ViewModel which tracks the viewed state for stories. + */ +class StoryViewStateCache() : Parcelable { + + private val viewStateMap: MutableMap = mutableMapOf() + + constructor(parcel: Parcel) : this() { + synchronized(this) { + val entries: Collection = ParcelUtil.readParcelableCollection(parcel, Entry::class.java) + entries.forEach { + viewStateMap[it.storyId] = it.hasSelfViewed + } + } + } + + fun putAll(cache: StoryViewStateCache) { + synchronized(this) { + viewStateMap.putAll(cache.viewStateMap) + } + } + + /** + * If storyId is in our map, return its value. Otherwise, insert and return the given state. + */ + fun getOrPut(storyId: Long, hasSelfViewed: Boolean): Boolean { + synchronized(this) { + return if (viewStateMap.containsKey(storyId)) { + viewStateMap[storyId]!! + } else { + viewStateMap[storyId] = hasSelfViewed + hasSelfViewed + } + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + ParcelUtil.writeParcelableCollection(parcel, viewStateMap.map { Entry(it.key, it.value) }) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): StoryViewStateCache { + return StoryViewStateCache(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + + @Parcelize + data class Entry( + val storyId: Long, + val hasSelfViewed: Boolean + ) : Parcelable +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewStateViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewStateViewModel.kt new file mode 100644 index 0000000000..b62e46c0ee --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewStateViewModel.kt @@ -0,0 +1,7 @@ +package org.thoughtcrime.securesms.stories.viewer.page + +import androidx.lifecycle.ViewModel + +class StoryViewStateViewModel : ViewModel() { + val storyViewStateCache = StoryViewStateCache() +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt index 1635a2d268..401d470b0d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt @@ -115,12 +115,15 @@ class StoryViewerPageFragment : private var volumeOutAnimator: Animator? = null private var volumeDebouncer: Debouncer = Debouncer(3, TimeUnit.SECONDS) + private val storyViewStateViewModel: StoryViewStateViewModel by viewModels() + private val viewModel: StoryViewerPageViewModel by viewModels( factoryProducer = { StoryViewerPageViewModel.Factory( storyViewerPageArgs, StoryViewerPageRepository( - requireContext() + requireContext(), + storyViewStateViewModel.storyViewStateCache ), StoryCache( GlideApp.with(requireActivity()), 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 e999fff02f..9b7f0ff5f6 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 @@ -30,7 +30,7 @@ import org.thoughtcrime.securesms.util.Base64 /** * Open for testing. */ -open class StoryViewerPageRepository(context: Context) { +open class StoryViewerPageRepository(context: Context, private val storyViewStateCache: StoryViewStateCache) { companion object { private val TAG = Log.tag(StoryViewerPageRepository::class.java) @@ -88,7 +88,7 @@ open class StoryViewerPageRepository(context: Context) { content = getContent(record as MmsMessageRecord), conversationMessage = ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, record), allowsReplies = record.storyType.isStoryWithReplies, - hasSelfViewed = if (record.isOutgoing) true else record.viewedReceiptCount > 0 + hasSelfViewed = storyViewStateCache.getOrPut(record.id, if (record.isOutgoing) true else record.viewedReceiptCount > 0) ) emitter.onNext(story)