172 lines
6.9 KiB
Kotlin
172 lines
6.9 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.PocketRecommendedStory
|
|
import mozilla.components.service.pocket.PocketStoriesService
|
|
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.
|
|
*
|
|
* @param pocketStoriesService [PocketStoriesService] used for updating details about the Pocket recommended stories.
|
|
* @param selectedPocketCategoriesDataStore [DataStore] used for reading or persisting details about the
|
|
* currently selected Pocket recommended stories categories.
|
|
* @param 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 -> {
|
|
persistStories(
|
|
coroutineScope = coroutineScope,
|
|
pocketStoriesService = pocketStoriesService,
|
|
updatedStories = action.storiesShown.map {
|
|
it.copy(timesShown = it.timesShown.inc())
|
|
}
|
|
)
|
|
}
|
|
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 persistStories(
|
|
coroutineScope: CoroutineScope,
|
|
pocketStoriesService: PocketStoriesService,
|
|
updatedStories: List<PocketRecommendedStory>
|
|
) {
|
|
coroutineScope.launch {
|
|
pocketStoriesService.updateStoriesTimesShown(
|
|
updatedStories
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
)
|
|
}
|
|
)
|
|
)
|
|
}
|
|
}
|