package com.kickstarter.viewmodels; import android.support.annotation.NonNull; import android.util.Pair; import com.kickstarter.libs.CurrentUserType; import com.kickstarter.libs.Environment; import com.kickstarter.libs.ActivityViewModel; import com.kickstarter.libs.preferences.BooleanPreferenceType; import com.kickstarter.libs.utils.ListUtils; import com.kickstarter.libs.utils.ObjectUtils; import com.kickstarter.libs.utils.UserUtils; import com.kickstarter.models.Category; import com.kickstarter.models.Project; import com.kickstarter.models.User; import com.kickstarter.services.ApiClientType; import com.kickstarter.services.DiscoveryParams; import com.kickstarter.services.apiresponses.DiscoverEnvelope; import com.kickstarter.ui.IntentKey; import com.kickstarter.ui.activities.ThanksActivity; import com.kickstarter.ui.viewholders.ThanksCategoryViewHolder; import com.kickstarter.ui.viewholders.ThanksProjectViewHolder; import com.kickstarter.viewmodels.inputs.ThanksViewModelInputs; import com.kickstarter.viewmodels.outputs.ThanksViewModelOutputs; 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.ignoreValues; import static com.kickstarter.libs.rx.transformers.Transformers.neverError; import static com.kickstarter.libs.rx.transformers.Transformers.takeWhen; import static com.kickstarter.libs.rx.transformers.Transformers.zipPair; import static com.kickstarter.libs.utils.BooleanUtils.isTrue; public final class ThanksViewModel extends ActivityViewModel<ThanksActivity> implements ThanksViewModelInputs, ThanksViewModelOutputs { private final ApiClientType apiClient; private final BooleanPreferenceType hasSeenAppRatingPreference; private final BooleanPreferenceType hasSeenGamesNewsletterPreference; private final CurrentUserType currentUser; // INPUTS private final PublishSubject<Void> shareClick = PublishSubject.create(); @Override public void shareClick() { shareClick.onNext(null); } private final PublishSubject<Void> shareOnFacebookClick = PublishSubject.create(); @Override public void shareOnFacebookClick() { shareOnFacebookClick.onNext(null); } private final PublishSubject<Void> shareOnTwitterClick = PublishSubject.create(); @Override public void shareOnTwitterClick() { shareOnTwitterClick.onNext(null); } private final PublishSubject<Void> signupToGamesNewsletterClick = PublishSubject.create(); @Override public void signupToGamesNewsletterClick() { signupToGamesNewsletterClick.onNext(null); } // THANKS PROJECT VIEW HOLDER INPUT private final PublishSubject<Project> projectClick = PublishSubject.create(); @Override public void projectClick(final @NonNull ThanksProjectViewHolder viewHolder, final @NonNull Project project) { projectClick.onNext(project); } // THANKS CATEGORY VIEW HOLDER INPUT private final PublishSubject<Category> categoryClick = PublishSubject.create(); @Override public void categoryClick(final @NonNull ThanksCategoryViewHolder viewHolder, final @NonNull Category category) { categoryClick.onNext(category); } // OUTPUTS private final BehaviorSubject<String> projectName = BehaviorSubject.create(); @Override public @NonNull Observable<String> projectName() { return projectName; } private final PublishSubject<Void> showConfirmGamesNewsletterDialog = PublishSubject.create(); @Override public @NonNull Observable<Void> showConfirmGamesNewsletterDialog() { return showConfirmGamesNewsletterDialog; } private final PublishSubject<Void> showGamesNewsletterDialog = PublishSubject.create(); @Override public @NonNull Observable<Void> showGamesNewsletterDialog() { return showGamesNewsletterDialog; } private final PublishSubject<Void> showRatingDialog = PublishSubject.create(); @Override public @NonNull Observable<Void> showRatingDialog() { return showRatingDialog; } private final BehaviorSubject<Pair<List<Project>, Category>> showRecommendations = BehaviorSubject.create(); @Override public @NonNull Observable<Pair<List<Project>, Category>> showRecommendations() { return showRecommendations; } private final PublishSubject<DiscoveryParams> startDiscovery = PublishSubject.create(); @Override public @NonNull Observable<DiscoveryParams> startDiscovery() { return startDiscovery; } private final PublishSubject<Project> startProject = PublishSubject.create(); @Override public @NonNull Observable<Project> startProject() { return startProject; } private final PublishSubject<Project> startShare = PublishSubject.create(); @Override public @NonNull Observable<Project> startShare() { return startShare; } private final PublishSubject<Project> startShareOnFacebook = PublishSubject.create(); @Override public @NonNull Observable<Project> startShareOnFacebook() { return startShareOnFacebook; } private final PublishSubject<Project> startShareOnTwitter = PublishSubject.create(); @Override public @NonNull Observable<Project> startShareOnTwitter() { return startShareOnTwitter; } private final PublishSubject<User> signedUpToGamesNewsletter = PublishSubject.create(); public final ThanksViewModelInputs inputs = this; public final ThanksViewModelOutputs outputs = this; public ThanksViewModel(final @NonNull Environment environment) { super(environment); apiClient = environment.apiClient(); currentUser = environment.currentUser(); hasSeenAppRatingPreference = environment.hasSeenAppRatingPreference(); hasSeenGamesNewsletterPreference = environment.hasSeenGamesNewsletterPreference(); final Observable<Project> project = intent() .map(i -> i.getParcelableExtra(IntentKey.PROJECT)) .ofType(Project.class) .take(1) .compose(bindToLifecycle()); final Observable<Category> rootCategory = project.flatMap(this::rootCategory); final Observable<Pair<List<Project>, Category>> projectsAndRootCategory = project .flatMap(this::relatedProjects) .compose(bindToLifecycle()) .compose(zipPair(rootCategory)); final Observable<Boolean> isGamesCategory = rootCategory .map(c -> "games".equals(c.slug())); final Observable<Boolean> hasSeenGamesNewsletterDialog = Observable.just(hasSeenGamesNewsletterPreference.get()); final Observable<Boolean> isSignedUpToGamesNewsletter = currentUser.observable() .map(u -> u != null && isTrue(u.gamesNewsletter())); final Observable<Boolean> showGamesNewsletter = Observable.combineLatest( isGamesCategory, hasSeenGamesNewsletterDialog, isSignedUpToGamesNewsletter, (isGames, hasSeen, isSignedUp) -> isGames && !hasSeen && !isSignedUp ) .take(1); project .map(Project::name) .compose(bindToLifecycle()) .subscribe(projectName::onNext); projectClick .compose(bindToLifecycle()) .subscribe(startProject::onNext); project .compose(takeWhen(shareClick)) .compose(bindToLifecycle()) .subscribe(startShare::onNext); project .compose(takeWhen(shareOnFacebookClick)) .compose(bindToLifecycle()) .subscribe(startShareOnFacebook::onNext); project .compose(takeWhen(shareOnTwitterClick)) .compose(bindToLifecycle()) .subscribe(startShareOnTwitter::onNext); categoryClick .map(c -> DiscoveryParams.builder().category(c).build()) .compose(bindToLifecycle()) .subscribe(startDiscovery::onNext); project .flatMap(this::relatedProjects) .compose(zipPair(rootCategory)) .compose(bindToLifecycle()) .subscribe(showRecommendations::onNext); Observable.just(hasSeenAppRatingPreference.get()) .take(1) .compose(combineLatestPair(showGamesNewsletter)) .filter(ag -> !ag.first && !ag.second) .compose(ignoreValues()) .compose(bindToLifecycle()) .subscribe(__ -> showRatingDialog.onNext(null)); showGamesNewsletter .filter(x -> x) .compose(bindToLifecycle()) .subscribe(__ -> showGamesNewsletterDialog.onNext(null)); showGamesNewsletterDialog .compose(bindToLifecycle()) .subscribe(__ -> hasSeenGamesNewsletterPreference.set(true)); currentUser.observable() .filter(ObjectUtils::isNotNull) .compose(takeWhen(signupToGamesNewsletterClick)) .flatMap(this::signupToGamesNewsletter) .compose(bindToLifecycle()) .subscribe(signedUpToGamesNewsletter::onNext); currentUser.observable() .filter(ObjectUtils::isNotNull) .compose(takeWhen(signedUpToGamesNewsletter)) .filter(UserUtils::isLocationGermany) .compose(bindToLifecycle()) .subscribe(__ -> showConfirmGamesNewsletterDialog.onNext(null)); // Event tracking categoryClick .compose(bindToLifecycle()) .subscribe(__ -> koala.trackCheckoutFinishJumpToDiscovery()); projectClick .compose(bindToLifecycle()) .subscribe(__ -> koala.trackCheckoutFinishJumpToProject()); shareClick .compose(bindToLifecycle()) .subscribe(__ -> koala.trackCheckoutShowShareSheet()); shareOnFacebookClick .compose(bindToLifecycle()) .subscribe(__ -> koala.trackCheckoutShowFacebookShareView()); shareOnTwitterClick .compose(bindToLifecycle()) .subscribe(__ -> koala.trackCheckoutShowTwitterShareView()); signedUpToGamesNewsletter .compose(bindToLifecycle()) .subscribe(__ -> koala.trackNewsletterToggle(true)); } /** * Given a project, returns an observable that emits the project's root category. */ private @NonNull Observable<Category> rootCategory(final @NonNull Project project) { final Category category = project.category(); if (category == null) { return Observable.empty(); } if (category.parent() != null) { return Observable.just(category.parent()); } return apiClient.fetchCategory(String.valueOf(category.rootId())) .compose(neverError()); } /** * Returns a shuffled list of 3 recommended projects, with fallbacks to similar and staff picked projects * for users with fewer than 3 recommendations. */ private @NonNull Observable<List<Project>> relatedProjects(final @NonNull Project project) { final DiscoveryParams recommendedParams = DiscoveryParams.builder() .backed(-1) .recommended(true) .perPage(6) .build(); final DiscoveryParams similarToParams = DiscoveryParams.builder() .backed(-1) .similarTo(project) .perPage(3) .build(); final Category category = project.category(); final DiscoveryParams staffPickParams = DiscoveryParams.builder() .category(category == null ? null : category.root()) .backed(-1) .staffPicks(true) .perPage(3) .build(); final Observable<Project> recommendedProjects = apiClient.fetchProjects(recommendedParams) .retry(2) .map(DiscoverEnvelope::projects) .map(ListUtils::shuffle) .flatMap(Observable::from) .take(3); final Observable<Project> similarToProjects = apiClient.fetchProjects(similarToParams) .retry(2) .map(DiscoverEnvelope::projects) .flatMap(Observable::from); final Observable<Project> staffPickProjects = apiClient.fetchProjects(staffPickParams) .retry(2) .map(DiscoverEnvelope::projects) .flatMap(Observable::from); return Observable.concat(recommendedProjects, similarToProjects, staffPickProjects) .compose(neverError()) .distinct() .take(3) .toList(); } private Observable<User> signupToGamesNewsletter(final @NonNull User user) { return apiClient .updateUserSettings(user.toBuilder().gamesNewsletter(true).build()) .compose(neverError()); } }