/* 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, private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.IO), ) : Middleware { override fun invoke( context: MiddlewareContext, 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 ) { 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, selectedPocketCategoriesDataStore: DataStore ) { 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, store: Store, selectedPocketCategoriesDataStore: DataStore ) { coroutineScope.launch { store.dispatch( AppAction.PocketStoriesCategoriesSelectionsChange( currentCategories, selectedPocketCategoriesDataStore.data.first() .valuesList.map { PocketRecommendedStoriesSelectedCategory( name = it.name, selectionTimestamp = it.selectionTimestamp ) } ) ) } }