You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
iceraven-browser/app/src/main/java/org/mozilla/fenix/home/PocketUpdatesMiddleware.kt

181 lines
7.3 KiB
Kotlin

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.fenix.home
import androidx.annotation.VisibleForTesting
import androidx.datastore.core.DataStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import mozilla.components.lib.state.Action
import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext
import mozilla.components.lib.state.Store
import mozilla.components.service.pocket.PocketStoriesService
import mozilla.components.service.pocket.PocketStory
import mozilla.components.service.pocket.PocketStory.PocketRecommendedStory
import mozilla.components.service.pocket.PocketStory.PocketSponsoredStory
import org.mozilla.fenix.components.AppStore
import org.mozilla.fenix.components.appstate.AppAction
import org.mozilla.fenix.components.appstate.AppState
import org.mozilla.fenix.datastore.SelectedPocketStoriesCategories
import org.mozilla.fenix.datastore.SelectedPocketStoriesCategories.SelectedPocketStoriesCategory
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesCategory
import org.mozilla.fenix.home.pocket.PocketRecommendedStoriesSelectedCategory
/**
* [AppStore] middleware reacting in response to Pocket related [Action]s.
*
* @property pocketStoriesService [PocketStoriesService] used for updating details about the Pocket recommended stories.
* @property selectedPocketCategoriesDataStore [DataStore] used for reading or persisting details about the
* currently selected Pocket recommended stories categories.
* @property coroutineScope [CoroutineScope] used for long running operations like disk IO.
*/
class PocketUpdatesMiddleware(
private val pocketStoriesService: PocketStoriesService,
private val selectedPocketCategoriesDataStore: DataStore<SelectedPocketStoriesCategories>,
private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO),
) : Middleware<AppState, AppAction> {
override fun invoke(
context: MiddlewareContext<AppState, AppAction>,
next: (AppAction) -> Unit,
action: AppAction,
) {
// Pre process actions
when (action) {
is AppAction.PocketStoriesCategoriesChange -> {
// Intercept the original action which would only update categories and
// dispatch a new action which also updates which categories are selected by the user
// from previous locally persisted data.
restoreSelectedCategories(
coroutineScope = coroutineScope,
currentCategories = action.storiesCategories,
store = context.store,
selectedPocketCategoriesDataStore = selectedPocketCategoriesDataStore,
)
}
else -> {
// no-op
}
}
next(action)
// Post process actions
when (action) {
is AppAction.PocketStoriesShown -> {
persistStoriesImpressions(
coroutineScope = coroutineScope,
pocketStoriesService = pocketStoriesService,
updatedStories = action.storiesShown,
)
}
is AppAction.SelectPocketStoriesCategory,
is AppAction.DeselectPocketStoriesCategory,
-> {
persistSelectedCategories(
coroutineScope = coroutineScope,
currentCategoriesSelections = context.state.pocketStoriesCategoriesSelections,
selectedPocketCategoriesDataStore = selectedPocketCategoriesDataStore,
)
}
else -> {
// no-op
}
}
}
}
/**
* Persist [updatedStories] for making their details available in between app restarts.
*
* @param coroutineScope [CoroutineScope] used for reading the locally persisted data.
* @param pocketStoriesService [PocketStoriesService] used for updating details about the Pocket recommended stories.
* @param updatedStories the list of stories to persist.
*/
@VisibleForTesting
internal fun persistStoriesImpressions(
coroutineScope: CoroutineScope,
pocketStoriesService: PocketStoriesService,
updatedStories: List<PocketStory>,
) {
coroutineScope.launch {
pocketStoriesService.updateStoriesTimesShown(
updatedStories.filterIsInstance<PocketRecommendedStory>()
.map {
it.copy(timesShown = it.timesShown.inc())
},
)
pocketStoriesService.recordStoriesImpressions(
updatedStories.filterIsInstance<PocketSponsoredStory>()
.map { it.id },
)
}
}
/**
* Persist [currentCategoriesSelections] for making this details available in between app restarts.
*
* @param coroutineScope [CoroutineScope] used for reading the locally persisted data.
* @param currentCategoriesSelections Currently selected Pocket recommended stories categories.
* @param selectedPocketCategoriesDataStore - DataStore used for persisting [currentCategoriesSelections].
*/
@VisibleForTesting
internal fun persistSelectedCategories(
coroutineScope: CoroutineScope,
currentCategoriesSelections: List<PocketRecommendedStoriesSelectedCategory>,
selectedPocketCategoriesDataStore: DataStore<SelectedPocketStoriesCategories>,
) {
val selectedCategories = currentCategoriesSelections
.map {
SelectedPocketStoriesCategory.newBuilder().apply {
name = it.name
selectionTimestamp = it.selectionTimestamp
}.build()
}
// Irrespective of the current selections or their number overwrite everything we had.
coroutineScope.launch {
selectedPocketCategoriesDataStore.updateData { data ->
data.newBuilderForType().addAllValues(selectedCategories).build()
}
}
}
/**
* Combines [currentCategories] with the locally persisted data about previously selected categories
* and emits a new [AppAction.PocketStoriesCategoriesSelectionsChange] to update these in store.
*
* @param coroutineScope [CoroutineScope] used for reading the locally persisted data.
* @param currentCategories Stories categories currently available
* @param store [Store] that will be updated.
* @param selectedPocketCategoriesDataStore [DataStore] containing details about the previously selected
* stories categories.
*/
@VisibleForTesting
internal fun restoreSelectedCategories(
coroutineScope: CoroutineScope,
currentCategories: List<PocketRecommendedStoriesCategory>,
store: Store<AppState, AppAction>,
selectedPocketCategoriesDataStore: DataStore<SelectedPocketStoriesCategories>,
) {
coroutineScope.launch {
store.dispatch(
AppAction.PocketStoriesCategoriesSelectionsChange(
currentCategories,
selectedPocketCategoriesDataStore.data.first()
.valuesList.map {
PocketRecommendedStoriesSelectedCategory(
name = it.name,
selectionTimestamp = it.selectionTimestamp,
)
},
),
)
}
}