package com.kickstarter.viewmodels;
import android.support.annotation.NonNull;
import android.util.Pair;
import com.kickstarter.libs.ActivityViewModel;
import com.kickstarter.libs.ApiPaginator;
import com.kickstarter.libs.CurrentUserType;
import com.kickstarter.libs.Either;
import com.kickstarter.libs.Environment;
import com.kickstarter.libs.KoalaContext;
import com.kickstarter.libs.utils.BooleanUtils;
import com.kickstarter.libs.utils.ObjectUtils;
import com.kickstarter.models.Comment;
import com.kickstarter.models.Project;
import com.kickstarter.models.Update;
import com.kickstarter.models.User;
import com.kickstarter.services.ApiClientType;
import com.kickstarter.services.apiresponses.CommentsEnvelope;
import com.kickstarter.services.apiresponses.ErrorEnvelope;
import com.kickstarter.ui.IntentKey;
import com.kickstarter.ui.activities.CommentsActivity;
import com.kickstarter.ui.adapters.data.CommentsData;
import com.kickstarter.viewmodels.inputs.CommentsViewModelInputs;
import com.kickstarter.viewmodels.outputs.CommentsViewModelOutputs;
import java.util.List;
import rx.Notification;
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.errors;
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.values;
public final class CommentsViewModel extends ActivityViewModel<CommentsActivity> implements CommentsViewModelInputs,
CommentsViewModelOutputs {
private final ApiClientType client;
private final CurrentUserType currentUser;
public CommentsViewModel(final @NonNull Environment environment) {
super(environment);
this.client = environment.apiClient();
this.currentUser = environment.currentUser();
final Observable<User> currentUser = Observable.merge(
this.currentUser.observable(),
this.loginSuccess.flatMap(__ -> this.client.fetchCurrentUser().compose(neverError())).share()
);
final Observable<Either<Project, Update>> projectOrUpdate = intent()
.take(1)
.map(i -> {
final Project project = i.getParcelableExtra(IntentKey.PROJECT);
return project != null
? new Either.Left<Project, Update>(project)
: new Either.Right<Project, Update>(i.getParcelableExtra(IntentKey.UPDATE));
})
.filter(ObjectUtils::isNotNull);
final Observable<Project> initialProject = projectOrUpdate
.flatMap(pOrU ->
pOrU.either(
Observable::just,
u -> client.fetchProject(String.valueOf(u.projectId())).compose(neverError())
)
)
.share();
final Observable<Project> project = Observable.merge(
initialProject,
initialProject
.compose(takeWhen(loginSuccess))
.flatMap(p -> client.fetchProject(p).compose(neverError()))
)
.share();
final Observable<Boolean> commentHasBody = commentBodyChanged
.map(body -> body.length() > 0);
final Observable<Notification<Comment>> commentNotification = projectOrUpdate
.compose(combineLatestPair(this.commentBodyChanged))
.compose(takeWhen(this.postCommentClicked))
.switchMap(projectOrUpdateAndBody ->
this.postComment(projectOrUpdateAndBody.first, projectOrUpdateAndBody.second)
.doOnSubscribe(() -> commentIsPosting.onNext(true))
.doAfterTerminate(() -> commentIsPosting.onNext(false))
.materialize()
)
.share();
final Observable<Comment> postedComment = commentNotification
.compose(values());
final Observable<Either<Project, Update>> startOverWith = Observable.merge(
projectOrUpdate,
projectOrUpdate.compose(takeWhen(refresh))
);
final ApiPaginator<Comment, CommentsEnvelope, Either<Project, Update>> paginator =
ApiPaginator.<Comment, CommentsEnvelope, Either<Project, Update>>builder()
.nextPage(nextPage)
.distinctUntilChanged(true)
.startOverWith(startOverWith)
.envelopeToListOfData(CommentsEnvelope::comments)
.envelopeToMoreUrl(env -> env.urls().api().moreComments())
.loadWithParams(pu -> pu.either(client::fetchComments, client::fetchComments))
.loadWithPaginationPath(client::fetchComments)
.build();
final Observable<List<Comment>> comments = paginator.paginatedData().share();
final Observable<Boolean> userCanComment = Observable.combineLatest(
currentUser,
project,
Pair::create
)
.map(userAndProject -> {
final User creator = userAndProject.second.creator();
final boolean currentUserIsCreator = userAndProject.first != null && userAndProject.first.id() == creator.id();
return currentUserIsCreator || userAndProject.second.isBacking();
});
final Observable<Project> commentableProject = Observable.combineLatest(
project,
userCanComment,
Pair::create
)
.filter(pc -> pc.second)
.map(pc -> pc.first);
commentNotification
.compose(errors())
.map(ErrorEnvelope::fromThrowable)
.subscribe(showPostCommentErrorToast::onNext);
commentableProject
.compose(takeWhen(loginSuccess))
.take(1)
.compose(bindToLifecycle())
.subscribe(p -> showCommentDialog.onNext(Pair.create(p, true)));
commentableProject
.compose(takeWhen(commentButtonClicked))
.compose(bindToLifecycle())
.subscribe(p -> showCommentDialog.onNext(Pair.create(p, true)));
commentDialogDismissed
.compose(bindToLifecycle())
.subscribe(__ -> {
showCommentDialog.onNext(null);
dismissCommentDialog.onNext(null);
});
// Seed comment body with user input.
commentBodyChanged
.compose(bindToLifecycle())
.subscribe(currentCommentBody::onNext);
Observable.combineLatest(
project,
comments,
currentUser,
CommentsData::deriveData
)
.compose(bindToLifecycle())
.subscribe(commentsData::onNext);
userCanComment
.map(BooleanUtils::negate)
.distinctUntilChanged()
.compose(bindToLifecycle())
.subscribe(commentButtonHidden::onNext);
postedComment
.compose(ignoreValues())
.compose(bindToLifecycle())
.subscribe(refresh::onNext);
commentHasBody
.compose(bindToLifecycle())
.subscribe(enablePostButton::onNext);
commentIsPosting
.map(b -> !b)
.compose(bindToLifecycle())
.subscribe(enablePostButton::onNext);
postedComment
.compose(bindToLifecycle())
.subscribe(__ -> {
commentDialogDismissed.onNext(null);
showCommentPostedToast.onNext(null);
});
postedComment
.map(__ -> "")
.compose(bindToLifecycle())
.subscribe(commentBodyChanged::onNext);
paginator.isFetching()
.compose(bindToLifecycle())
.subscribe(isFetchingComments);
project
.take(1)
.compose(bindToLifecycle())
.subscribe(__ -> refresh.onNext(null));
final Observable<Update> update = projectOrUpdate.map(Either::right);
// TODO: add a pageCount to RecyclerViewPaginator to track loading newer comments.
Observable.combineLatest(project, update, Pair::create)
.compose(takeWhen(nextPage))
.compose(bindToLifecycle())
.subscribe(pu ->
koala.trackLoadedOlderComments(
pu.first, pu.second, pu.second == null ? KoalaContext.Comments.PROJECT : KoalaContext.Comments.UPDATE
)
);
Observable.combineLatest(project, update, Pair::create)
.take(1)
.compose(bindToLifecycle())
.subscribe(pu ->
koala.trackViewedComments(
pu.first, pu.second, pu.second == null ? KoalaContext.Comments.PROJECT : KoalaContext.Comments.UPDATE
)
);
Observable.combineLatest(project, update, Pair::create)
.compose(takeWhen(postedComment))
.compose(bindToLifecycle())
.subscribe(pu ->
koala.trackPostedComment(
pu.first,
pu.second,
pu.second == null ? KoalaContext.CommentDialog.PROJECT_COMMENTS : KoalaContext.CommentDialog.UPDATE_COMMENTS
)
);
projectOrUpdate
.filter(Either::isLeft)
.map(Either::left)
.compose(takeWhen(nextPage))
.compose(bindToLifecycle())
.subscribe(koala::trackLoadedOlderProjectComments);
projectOrUpdate
.filter(Either::isLeft)
.map(Either::left)
.compose(bindToLifecycle())
.subscribe(koala::trackProjectCommentsView);
projectOrUpdate
.filter(Either::isLeft)
.map(Either::left)
.compose(takeWhen(postedComment))
.compose(bindToLifecycle())
.subscribe(koala::trackProjectCommentCreate);
}
private @NonNull Observable<Comment> postComment(final @NonNull Either<Project, Update> projectOrUpdate, final @NonNull String body) {
return projectOrUpdate.either(
p -> client.postComment(p, body),
u -> client.postComment(u, body)
);
}
private final PublishSubject<String> commentBodyChanged = PublishSubject.create();
private final PublishSubject<Void> commentButtonClicked = PublishSubject.create();
private final PublishSubject<Void> commentDialogDismissed = PublishSubject.create();
private final PublishSubject<Boolean> commentIsPosting = PublishSubject.create();
private final PublishSubject<Void> loginSuccess = PublishSubject.create();
private final PublishSubject<Void> nextPage = PublishSubject.create();
private final PublishSubject<Void> postCommentClicked = PublishSubject.create();
private final PublishSubject<Void> refresh = PublishSubject.create();
private final BehaviorSubject<CommentsData> commentsData = BehaviorSubject.create();
private final BehaviorSubject<String> currentCommentBody = BehaviorSubject.create();
private final BehaviorSubject<Void> dismissCommentDialog = BehaviorSubject.create();
private final BehaviorSubject<Boolean> enablePostButton = BehaviorSubject.create();
private final BehaviorSubject<Boolean> isFetchingComments = BehaviorSubject.create();
private final BehaviorSubject<Boolean> commentButtonHidden = BehaviorSubject.create();
private final BehaviorSubject<Pair<Project, Boolean>> showCommentDialog = BehaviorSubject.create();
private final PublishSubject<Void> showCommentPostedToast = PublishSubject.create();
private final PublishSubject<ErrorEnvelope> showPostCommentErrorToast = PublishSubject.create();
public final CommentsViewModelInputs inputs = this;
public final CommentsViewModelOutputs outputs = this;
@Override public void commentBodyChanged(final @NonNull String string) {
commentBodyChanged.onNext(string);
}
@Override public void commentButtonClicked() {
commentButtonClicked.onNext(null);
}
@Override public void commentDialogDismissed() {
commentDialogDismissed.onNext(null);
}
@Override public void loginSuccess() {
loginSuccess.onNext(null);
}
@Override public void nextPage() {
nextPage.onNext(null);
}
@Override public void postCommentClicked() {
postCommentClicked.onNext(null);
}
@Override public void refresh() {
refresh.onNext(null);
}
@Override public @NonNull Observable<CommentsData> commentsData() {
return commentsData;
}
@Override public @NonNull Observable<String> currentCommentBody() {
return currentCommentBody;
}
@Override public @NonNull Observable<Void> dismissCommentDialog() {
return dismissCommentDialog;
}
@Override public @NonNull Observable<Boolean> enablePostButton() {
return enablePostButton;
}
@Override public @NonNull Observable<Boolean> isFetchingComments() {
return isFetchingComments;
}
@Override public @NonNull Observable<Boolean> commentButtonHidden() {
return commentButtonHidden;
}
@Override public @NonNull Observable<Pair<Project, Boolean>> showCommentDialog() {
return showCommentDialog;
}
@Override public @NonNull Observable<Void> showCommentPostedToast() {
return showCommentPostedToast;
}
@Override public @NonNull Observable<String> showPostCommentErrorToast() {
return showPostCommentErrorToast
.map(ErrorEnvelope::errorMessage);
}
}