package com.kickstarter.viewmodels; import android.content.Intent; import android.support.annotation.NonNull; import android.util.Pair; import com.kickstarter.libs.ActivityViewModel; import com.kickstarter.libs.BuildCheck; import com.kickstarter.libs.CurrentUserType; import com.kickstarter.libs.Environment; import com.kickstarter.libs.utils.BooleanUtils; import com.kickstarter.libs.utils.DiscoveryDrawerUtils; import com.kickstarter.libs.utils.DiscoveryUtils; import com.kickstarter.models.Category; import com.kickstarter.models.User; import com.kickstarter.services.ApiClientType; import com.kickstarter.services.DiscoveryParams; import com.kickstarter.services.WebClientType; import com.kickstarter.services.apiresponses.InternalBuildEnvelope; import com.kickstarter.ui.activities.DiscoveryActivity; import com.kickstarter.ui.adapters.DiscoveryPagerAdapter; import com.kickstarter.ui.adapters.data.NavigationDrawerData; import com.kickstarter.ui.intentmappers.DiscoveryIntentMapper; import com.kickstarter.ui.intentmappers.IntentMapper; import com.kickstarter.ui.viewholders.discoverydrawer.ChildFilterViewHolder; import com.kickstarter.ui.viewholders.discoverydrawer.LoggedInViewHolder; import com.kickstarter.ui.viewholders.discoverydrawer.LoggedOutViewHolder; import com.kickstarter.ui.viewholders.discoverydrawer.ParentFilterViewHolder; import com.kickstarter.ui.viewholders.discoverydrawer.TopFilterViewHolder; import com.kickstarter.viewmodels.inputs.DiscoveryViewModelInputs; import com.kickstarter.viewmodels.outputs.DiscoveryViewModelOutputs; import java.util.List; import rx.Observable; import rx.subjects.BehaviorSubject; import rx.subjects.PublishSubject; import static com.kickstarter.libs.rx.transformers.Transformers.combineLatestPair; import static com.kickstarter.libs.rx.transformers.Transformers.neverError; import static com.kickstarter.libs.rx.transformers.Transformers.takeWhen; public final class DiscoveryViewModel extends ActivityViewModel<DiscoveryActivity> implements DiscoveryViewModelInputs, DiscoveryViewModelOutputs { private final ApiClientType apiClient; private final WebClientType webClient; private final BuildCheck buildCheck; private final CurrentUserType currentUser; public DiscoveryViewModel(final @NonNull Environment environment) { super(environment); apiClient = environment.apiClient(); buildCheck = environment.buildCheck(); currentUser = environment.currentUser(); webClient = environment.webClient(); buildCheck.bind(this, webClient); showBuildCheckAlert = newerBuildIsAvailable; showInternalTools = internalToolsClick; showLoginTout = loggedOutLoginToutClick; showProfile = profileClick; showSettings = settingsClick; // Seed params when we are freshly launching the app with no data. final Observable<DiscoveryParams> paramsFromInitialIntent = intent() .take(1) .map(Intent::getAction) .filter(Intent.ACTION_MAIN::equals) .map(__ -> DiscoveryParams.builder().build()) .share(); final Observable<DiscoveryParams> paramsFromIntent = intent() .flatMap(i -> DiscoveryIntentMapper.params(i, apiClient)); final Observable<DiscoveryParams> drawerParamsClicked = childFilterRowClick .mergeWith(topFilterRowClick) .map(NavigationDrawerData.Section.Row::params); // Merge various param data sources. final Observable<DiscoveryParams> params = Observable.merge( paramsFromInitialIntent, paramsFromIntent, drawerParamsClicked ); final Observable<Integer> pagerSelectedPage = pagerSetPrimaryPage.distinctUntilChanged(); // Combine params with the selected sort position. Observable.combineLatest( params, pagerSelectedPage.map(DiscoveryUtils::sortFromPosition), (p, s) -> p.toBuilder().sort(s).build() ) .compose(bindToLifecycle()) .subscribe(updateParamsForPage); final Observable<List<Category>> categories = apiClient.fetchCategories() .compose(neverError()) .flatMap(Observable::from) .toSortedList() .share(); // Combine root categories with the selected sort position. Observable.combineLatest( categories .flatMap(Observable::from) .filter(Category::isRoot) .toList(), pagerSelectedPage, Pair::create ) .compose(bindToLifecycle()) .subscribe(rootCategoriesAndPosition); final Observable<Category> drawerClickedParentCategory = parentFilterRowClick .map(NavigationDrawerData.Section.Row::params) .map(DiscoveryParams::category); final Observable<Category> expandedCategory = Observable.merge( topFilterRowClick.map(__ -> (Category) null), drawerClickedParentCategory ) .scan(null, (previous, next) -> { if (previous != null && next != null && previous.equals(next)) { return null; } return next; }); // Accumulate a list of pages to clear when the params or user changes, // to avoid displaying old data. pagerSelectedPage .compose(takeWhen(params)) .compose(combineLatestPair(currentUser.observable())) .map(pageAndUser -> pageAndUser.first) .flatMap(currentPage -> Observable.from(DiscoveryParams.Sort.values()) .map(DiscoveryUtils::positionFromSort) .filter(sortPosition -> !sortPosition.equals(currentPage)) .toList() ) .compose(bindToLifecycle()) .subscribe(clearPages); params.distinctUntilChanged() .compose(bindToLifecycle()) .subscribe(updateToolbarWithParams); updateParamsForPage.map(__ -> true) .compose(bindToLifecycle()) .subscribe(expandSortTabLayout); Observable.combineLatest( categories, params, expandedCategory, currentUser.observable(), DiscoveryDrawerUtils::deriveNavigationDrawerData ) .distinctUntilChanged() .compose(bindToLifecycle()) .subscribe(navigationDrawerData); drawerParamsClicked .compose(bindToLifecycle()) .subscribe(koala::trackDiscoveryFilterSelected); Observable.merge( openDrawer, childFilterRowClick.map(__ -> false), topFilterRowClick.map(__ -> false), internalToolsClick.map(__ -> false), loggedOutLoginToutClick.map(__ -> false), profileClick.map(__ -> false), settingsClick.map(__ -> false) ) .distinctUntilChanged() .compose(bindToLifecycle()) .subscribe(drawerIsOpen); openDrawer .filter(BooleanUtils::isTrue) .compose(bindToLifecycle()) .subscribe(__ -> koala.trackDiscoveryFilters()); intent() .filter(IntentMapper::appBannerIsSet) .compose(bindToLifecycle()) .subscribe(__ -> koala.trackOpenedAppBanner()); } private final PublishSubject<NavigationDrawerData.Section.Row> childFilterRowClick = PublishSubject.create(); private final PublishSubject<Void> internalToolsClick = PublishSubject.create(); private final PublishSubject<Void> loggedOutLoginToutClick = PublishSubject.create(); private final PublishSubject<InternalBuildEnvelope> newerBuildIsAvailable = PublishSubject.create(); private final PublishSubject<Boolean> openDrawer = PublishSubject.create(); private final PublishSubject<Integer> pagerSetPrimaryPage = PublishSubject.create(); private final PublishSubject<NavigationDrawerData.Section.Row> parentFilterRowClick = PublishSubject.create(); private final PublishSubject<Void> profileClick = PublishSubject.create(); private final PublishSubject<Void> settingsClick = PublishSubject.create(); private final PublishSubject<NavigationDrawerData.Section.Row> topFilterRowClick = PublishSubject.create(); private final BehaviorSubject<List<Integer>> clearPages = BehaviorSubject.create(); private final BehaviorSubject<Boolean> drawerIsOpen = BehaviorSubject.create(); private final BehaviorSubject<Boolean> expandSortTabLayout = BehaviorSubject.create(); private final BehaviorSubject<NavigationDrawerData> navigationDrawerData = BehaviorSubject.create(); private final BehaviorSubject<Pair<List<Category>, Integer>> rootCategoriesAndPosition = BehaviorSubject.create(); private final Observable<InternalBuildEnvelope> showBuildCheckAlert; private final Observable<Void> showInternalTools; private final Observable<Void> showLoginTout; private final Observable<Void> showProfile; private final Observable<Void> showSettings; private final BehaviorSubject<DiscoveryParams> updateParamsForPage = BehaviorSubject.create(); private final BehaviorSubject<DiscoveryParams> updateToolbarWithParams = BehaviorSubject.create(); public final DiscoveryViewModelInputs inputs = this; public final DiscoveryViewModelOutputs outputs = this; @Override public void childFilterViewHolderRowClick(final @NonNull ChildFilterViewHolder viewHolder, final @NonNull NavigationDrawerData.Section.Row row) { childFilterRowClick.onNext(row); } @Override public void discoveryPagerAdapterSetPrimaryPage(final @NonNull DiscoveryPagerAdapter adapter, final int position) { pagerSetPrimaryPage.onNext(position); } @Override public void loggedInViewHolderInternalToolsClick(final @NonNull LoggedInViewHolder viewHolder) { internalToolsClick.onNext(null); } @Override public void loggedInViewHolderProfileClick(final @NonNull LoggedInViewHolder viewHolder, final @NonNull User user) { profileClick.onNext(null); } @Override public void loggedInViewHolderSettingsClick(final @NonNull LoggedInViewHolder viewHolder, final @NonNull User user) { settingsClick.onNext(null); } @Override public void loggedOutViewHolderInternalToolsClick(final @NonNull LoggedOutViewHolder viewHolder) { internalToolsClick.onNext(null); } @Override public void loggedOutViewHolderLoginToutClick(final @NonNull LoggedOutViewHolder viewHolder) { loggedOutLoginToutClick.onNext(null); } @Override public void newerBuildIsAvailable(final @NonNull InternalBuildEnvelope envelope) { newerBuildIsAvailable.onNext(envelope); } @Override public void openDrawer(final boolean open) { openDrawer.onNext(open); } @Override public void parentFilterViewHolderRowClick(final @NonNull ParentFilterViewHolder viewHolder, final @NonNull NavigationDrawerData.Section.Row row) { parentFilterRowClick.onNext(row); } @Override public void topFilterViewHolderRowClick(final @NonNull TopFilterViewHolder viewHolder, final @NonNull NavigationDrawerData.Section.Row row) { topFilterRowClick.onNext(row); } @Override public @NonNull Observable<List<Integer>> clearPages() { return clearPages; } @Override public @NonNull Observable<Boolean> drawerIsOpen() { return drawerIsOpen; } @Override public @NonNull Observable<Boolean> expandSortTabLayout() { return expandSortTabLayout; } @Override public @NonNull Observable<NavigationDrawerData> navigationDrawerData() { return navigationDrawerData; } @Override public @NonNull Observable<Pair<List<Category>, Integer>> rootCategoriesAndPosition() { return rootCategoriesAndPosition; } @Override public @NonNull Observable<InternalBuildEnvelope> showBuildCheckAlert() { return showBuildCheckAlert; } @Override public @NonNull Observable<Void> showInternalTools() { return showInternalTools; } @Override public @NonNull Observable<Void> showLoginTout() { return showLoginTout; } @Override public @NonNull Observable<Void> showProfile() { return showProfile; } @Override public @NonNull Observable<Void> showSettings() { return showSettings; } @Override public @NonNull Observable<DiscoveryParams> updateParamsForPage() { return updateParamsForPage; } @Override public @NonNull Observable<DiscoveryParams> updateToolbarWithParams() { return updateToolbarWithParams; } }