package com.kickstarter.viewmodels; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.util.Pair; import com.kickstarter.libs.ApiPaginator; import com.kickstarter.libs.CurrentUserType; import com.kickstarter.libs.Environment; import com.kickstarter.libs.FragmentViewModel; import com.kickstarter.libs.KoalaContext; import com.kickstarter.libs.RefTag; import com.kickstarter.libs.preferences.IntPreferenceType; import com.kickstarter.libs.utils.DiscoveryParamsUtils; import com.kickstarter.libs.utils.DiscoveryUtils; import com.kickstarter.libs.utils.ListUtils; import com.kickstarter.libs.utils.ObjectUtils; import com.kickstarter.models.Activity; import com.kickstarter.models.Category; import com.kickstarter.models.Project; import com.kickstarter.services.ApiClientType; import com.kickstarter.services.DiscoveryParams; import com.kickstarter.services.apiresponses.ActivityEnvelope; import com.kickstarter.services.apiresponses.DiscoverEnvelope; import com.kickstarter.ui.fragments.DiscoveryFragment; import com.kickstarter.ui.viewholders.ActivitySampleFriendBackingViewHolder; import com.kickstarter.ui.viewholders.ActivitySampleFriendFollowViewHolder; import com.kickstarter.ui.viewholders.ActivitySampleProjectViewHolder; import com.kickstarter.ui.viewholders.DiscoveryOnboardingViewHolder; import com.kickstarter.ui.viewholders.ProjectCardViewHolder; import com.kickstarter.viewmodels.outputs.DiscoveryFragmentViewModelInputs; import com.kickstarter.viewmodels.outputs.DiscoveryFragmentViewModelOutputs; import java.util.ArrayList; 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.takePairWhen; import static com.kickstarter.libs.utils.BooleanUtils.isTrue; public final class DiscoveryFragmentViewModel extends FragmentViewModel<DiscoveryFragment> implements DiscoveryFragmentViewModelInputs, DiscoveryFragmentViewModelOutputs { private final ApiClientType apiClient; private final CurrentUserType currentUser; private final IntPreferenceType activitySamplePreference; public DiscoveryFragmentViewModel(final @NonNull Environment environment) { super(environment); apiClient = environment.apiClient(); activitySamplePreference = environment.activitySamplePreference(); currentUser = environment.currentUser(); final Observable<DiscoveryParams> selectedParams = Observable.combineLatest( currentUser.observable(), paramsFromActivity.distinctUntilChanged(), (__, params) -> params ); final ApiPaginator<Project, DiscoverEnvelope, DiscoveryParams> paginator = ApiPaginator.<Project, DiscoverEnvelope, DiscoveryParams>builder() .nextPage(nextPage) .startOverWith(selectedParams) .envelopeToListOfData(DiscoverEnvelope::projects) .envelopeToMoreUrl(env -> env.urls().api().moreProjects()) .loadWithParams(apiClient::fetchProjects) .loadWithPaginationPath(apiClient::fetchProjects) .clearWhenStartingOver(true) .concater(ListUtils::concatDistinct) .build(); final Observable<Pair<Project, RefTag>> projectCardClick = paramsFromActivity .compose(takePairWhen(clickProject)) .map(pp -> DiscoveryFragmentViewModel.projectAndRefTagFromParamsAndProject(pp.first, pp.second)); final Observable<Pair<Project, RefTag>> activitySampleProjectClick = this.activitySampleProjectClick .map(p -> Pair.create(p, RefTag.activitySample())); Observable.combineLatest( paginator.paginatedData(), rootCategories, DiscoveryUtils::fillRootCategoryForFeaturedProjects ) .compose(bindToLifecycle()) .subscribe(projects); showActivityFeed = activityClick; showActivityUpdate = activityUpdateClick; showLoginTout = discoveryOnboardingLoginToutClick; Observable.merge( projectCardClick, activitySampleProjectClick ) .compose(bindToLifecycle()) .subscribe(showProject); clearPage .compose(bindToLifecycle()) .subscribe(__ -> { shouldShowOnboardingView.onNext(false); activity.onNext(null); projects.onNext(new ArrayList<>()); }); paramsFromActivity .compose(combineLatestPair(currentUser.isLoggedIn())) .map(pu -> isOnboardingVisible(pu.first, pu.second)) .compose(bindToLifecycle()) .subscribe(shouldShowOnboardingView); currentUser.loggedInUser() .compose(combineLatestPair(paramsFromActivity)) .flatMap(__ -> this.fetchActivity()) .filter(this::activityHasNotBeenSeen) .doOnNext(this::saveLastSeenActivityId) .compose(bindToLifecycle()) .subscribe(activity); // Clear activity sample when params change paramsFromActivity .map(__ -> (Activity) null) .compose(bindToLifecycle()) .subscribe(activity); paramsFromActivity .compose(combineLatestPair(paginator.loadingPage().distinctUntilChanged())) .map(paramsAndPage -> paramsAndPage.first.toBuilder().page(paramsAndPage.second).build()) .compose(combineLatestPair(currentUser.isLoggedIn())) .compose(bindToLifecycle()) .subscribe(paramsAndLoggedIn -> { koala.trackDiscovery( paramsAndLoggedIn.first, isOnboardingVisible(paramsAndLoggedIn.first, paramsAndLoggedIn.second) ); }); showActivityUpdate .map(Activity::project) .filter(ObjectUtils::isNotNull) .compose(bindToLifecycle()) .subscribe(p -> koala.trackViewedUpdate(p, KoalaContext.Update.ACTIVITY_SAMPLE)); } private boolean activityHasNotBeenSeen(final @Nullable Activity activity) { return activity != null && activity.id() != activitySamplePreference.get(); } private Observable<Activity> fetchActivity() { return apiClient.fetchActivities(1) .map(ActivityEnvelope::activities) .map(ListUtils::first) .filter(ObjectUtils::isNotNull) .compose(neverError()); } private boolean isOnboardingVisible(final @NonNull DiscoveryParams params, final boolean isLoggedIn) { final DiscoveryParams.Sort sort = params.sort(); final boolean isSortHome = DiscoveryParams.Sort.HOME.equals(sort); return isTrue(params.isAllProjects()) && isSortHome && !isLoggedIn; } /** * Converts a pair (params, project) into a (project, refTag) pair that does some extra logic around POTD and * featured projects.. */ private static @NonNull Pair<Project, RefTag> projectAndRefTagFromParamsAndProject(final @NonNull DiscoveryParams params, final @NonNull Project project) { final RefTag refTag; if (project.isPotdToday()) { refTag = RefTag.discoverPotd(); } else if (project.isFeaturedToday()) { refTag = RefTag.categoryFeatured(); } else { refTag = DiscoveryParamsUtils.refTag(params); } return new Pair<>(project, refTag); } private void saveLastSeenActivityId(final @Nullable Activity activity) { if (activity != null) { activitySamplePreference.set((int) activity.id()); } } private final PublishSubject<Boolean> activityClick = PublishSubject.create(); private final PublishSubject<Project> activitySampleProjectClick = PublishSubject.create(); private final PublishSubject<Activity> activityUpdateClick = PublishSubject.create(); private final PublishSubject<Void> clearPage = PublishSubject.create(); private final PublishSubject<Project> clickProject = PublishSubject.create(); private final PublishSubject<Boolean> discoveryOnboardingLoginToutClick = PublishSubject.create(); private final PublishSubject<Void> nextPage = PublishSubject.create(); private final PublishSubject<DiscoveryParams> paramsFromActivity = PublishSubject.create(); private final PublishSubject<List<Category>> rootCategories = PublishSubject.create(); private final BehaviorSubject<Activity> activity = BehaviorSubject.create(); private final BehaviorSubject<List<Project>> projects = BehaviorSubject.create(); private final Observable<Boolean> showActivityFeed; private final Observable<Activity> showActivityUpdate; private final Observable<Boolean> showLoginTout; private final PublishSubject<Pair<Project, RefTag>> showProject = PublishSubject.create(); private final BehaviorSubject<Boolean> shouldShowOnboardingView = BehaviorSubject.create(); public final DiscoveryFragmentViewModelInputs inputs = this; public final DiscoveryFragmentViewModelOutputs outputs = this; @Override public void activitySampleFriendBackingViewHolderProjectClicked(final @NonNull ActivitySampleFriendBackingViewHolder viewHolder, final @NonNull Project project) { activitySampleProjectClick.onNext(project); } @Override public void activitySampleFriendBackingViewHolderSeeActivityClicked(final @NonNull ActivitySampleFriendBackingViewHolder viewHolder) { activityClick.onNext(true); } @Override public void activitySampleFriendFollowViewHolderSeeActivityClicked(final @NonNull ActivitySampleFriendFollowViewHolder viewHolder) { activityClick.onNext(true); } @Override public void activitySampleProjectViewHolderProjectClicked(final @NonNull ActivitySampleProjectViewHolder viewHolder, final @NonNull Project project) { activitySampleProjectClick.onNext(project); } @Override public void activitySampleProjectViewHolderSeeActivityClicked(final @NonNull ActivitySampleProjectViewHolder viewHolder) { activityClick.onNext(true); } @Override public void activitySampleProjectViewHolderUpdateClicked(final @NonNull ActivitySampleProjectViewHolder viewHolder, final @NonNull Activity activity) { activityUpdateClick.onNext(activity); } @Override public void rootCategories(final @NonNull List<Category> rootCategories) { this.rootCategories.onNext(rootCategories); } @Override public void clearPage() { clearPage.onNext(null); } @Override public void discoveryOnboardingViewHolderLoginToutClick(final @NonNull DiscoveryOnboardingViewHolder viewHolder) { discoveryOnboardingLoginToutClick.onNext(true); } @Override public void nextPage() { nextPage.onNext(null); } @Override public void paramsFromActivity(final @NonNull DiscoveryParams params) { paramsFromActivity.onNext(params); } @Override public void projectCardViewHolderClick(final @NonNull ProjectCardViewHolder viewHolder, final @NonNull Project project) { clickProject.onNext(project); } @Override public @NonNull Observable<Activity> activity() { return activity; } @Override public @NonNull Observable<List<Project>> projects() { return projects; } @Override public @NonNull Observable<Boolean> showActivityFeed() { return showActivityFeed; } @Override public @NonNull Observable<Activity> showActivityUpdate() { return showActivityUpdate; } @Override public @NonNull Observable<Boolean> showLoginTout() { return showLoginTout; } @Override public @NonNull Observable<Pair<Project, RefTag>> showProject() { return showProject; } @Override public @NonNull Observable<Boolean> shouldShowOnboardingView() { return shouldShowOnboardingView; } }