From e33d7964c45d623d9b8d9870a89b3e201fadedf9 Mon Sep 17 00:00:00 2001 From: hadia Date: Thu, 15 Jul 2021 18:46:38 +0200 Subject: [PATCH 1/9] Adding alert dialog --- .../libs/utils/extensions/ContextExt.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/app/src/main/java/com/kickstarter/libs/utils/extensions/ContextExt.kt b/app/src/main/java/com/kickstarter/libs/utils/extensions/ContextExt.kt index 84578667f8..2dfc1c17f3 100644 --- a/app/src/main/java/com/kickstarter/libs/utils/extensions/ContextExt.kt +++ b/app/src/main/java/com/kickstarter/libs/utils/extensions/ContextExt.kt @@ -1,6 +1,7 @@ @file:JvmName("ContextExt") package com.kickstarter.libs.utils.extensions +import android.app.AlertDialog import android.app.Application import android.content.Context import com.kickstarter.KSApplication @@ -18,3 +19,36 @@ fun Context.registerActivityLifecycleCallbacks(callbacks: Application.ActivityLi this.registerActivityLifecycleCallbacks(callbacks) } } + +fun Context.showAlertDialog( + title: String? = "", + message: String? = "", + positiveActionTitle: String? = null, + negativeActionTitle: String? = null, + isCancelable: Boolean = true, + positiveAction: (() -> Unit)? = null, + negativeAction: (() -> Unit)? = null +) { + + // setup the alert builder + val builder = AlertDialog.Builder(this).apply { + setTitle(title) + setMessage(message) + + // add a button + setPositiveButton(positiveActionTitle) { dialog, _ -> + dialog.dismiss() + positiveAction?.invoke() + } + + setNegativeButton(negativeActionTitle) { dialog, _ -> + dialog.dismiss() + negativeAction?.invoke() + } + setCancelable(isCancelable) + } + + // create and show the alert dialog + val dialog: AlertDialog = builder.create() + dialog.show() +} From ecc0367b05f0db059ae4e34076487f5a4eea7bb3 Mon Sep 17 00:00:00 2001 From: hadia Date: Thu, 15 Jul 2021 18:47:04 +0200 Subject: [PATCH 2/9] Handling Alert dialog in comment activity --- .../ui/activities/CommentsActivity.kt | 47 ++++++++++++++++++- .../viewmodels/CommentsViewModel.kt | 23 ++++++++- app/src/main/res/values/strings.xml | 4 ++ 3 files changed, 71 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt b/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt index 3780e92fc2..0334db4c60 100644 --- a/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt +++ b/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt @@ -14,6 +14,7 @@ import com.kickstarter.libs.qualifiers.RequiresActivityViewModel import com.kickstarter.libs.utils.ApplicationUtils import com.kickstarter.libs.utils.TransitionUtils import com.kickstarter.libs.utils.UrlUtils +import com.kickstarter.libs.utils.extensions.showAlertDialog import com.kickstarter.libs.utils.extensions.toVisibility import com.kickstarter.models.Comment import com.kickstarter.ui.IntentKey @@ -44,7 +45,9 @@ class CommentsActivity : setContentView(view) binding.commentsRecyclerView.adapter = adapter - binding.backButton.setOnClickListener { viewModel.inputs.backPressed() } + binding.backButton.setOnClickListener { + viewModel.inputs.checkIfThereAnyPendingComments(true) + } setupPagination() @@ -130,6 +133,43 @@ class CommentsActivity : .subscribe { ApplicationUtils.openUrlExternally(this, UrlUtils.appendPath(environment().webEndpoint(), COMMENT_KICKSTARTER_GUIDELINES)) } + + viewModel.outputs.hasPendingComments() + .compose(bindToLifecycle()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + if (it.first) { + handleBackAction(it.second) + } else { + executeActions(it.second) + } + } + } + + private fun handleBackAction(isBackAction: Boolean) { + this.showAlertDialog( + getString(R.string.Your_comment_wasnt_posted), + getString(R.string.You_will_lose_the_comment), + getString(R.string.cancel), + getString(R.string.leave_page), + false, + positiveAction = { + executeActions(isBackAction) + }, + negativeAction = { + viewModel.inputs.backPressed() + } + ) + } + + fun executeActions(isBackAction: Boolean) { + if (!isBackAction) { + viewModel.inputs.refresh() + } + } + + override fun back() { + viewModel.inputs.checkIfThereAnyPendingComments(true) } private fun closeCommentsActivity() { @@ -142,7 +182,10 @@ class CommentsActivity : recyclerViewPaginator = RecyclerViewPaginator(binding.commentsRecyclerView, { viewModel.inputs.nextPage() }, viewModel.outputs.isFetchingComments()) swipeRefresher = SwipeRefresher( - this, binding.commentsSwipeRefreshLayout, { viewModel.inputs.refresh() } + this, binding.commentsSwipeRefreshLayout, + { + viewModel.inputs.checkIfThereAnyPendingComments(false) + } ) { viewModel.outputs.isFetchingComments() } viewModel.outputs.isFetchingComments() diff --git a/app/src/main/java/com/kickstarter/viewmodels/CommentsViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/CommentsViewModel.kt index da3974d69d..6e917e5e62 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/CommentsViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/CommentsViewModel.kt @@ -9,6 +9,7 @@ import com.kickstarter.libs.Environment import com.kickstarter.libs.loadmore.ApolloPaginate import com.kickstarter.libs.rx.transformers.Transformers import com.kickstarter.libs.rx.transformers.Transformers.combineLatestPair +import com.kickstarter.libs.rx.transformers.Transformers.takePairWhen import com.kickstarter.libs.utils.ProjectUtils import com.kickstarter.models.Comment import com.kickstarter.models.Project @@ -36,6 +37,7 @@ interface CommentsViewModel { fun insertNewCommentToList(comment: String, createdAt: DateTime) fun onReplyClicked(comment: Comment, openKeyboard: Boolean) fun onShowGuideLinesLinkClicked() + fun checkIfThereAnyPendingComments(isBackAction: Boolean) /** Will be called with the successful response when calling the `postComment` Mutation **/ fun refreshComment(comment: Comment) @@ -55,6 +57,7 @@ interface CommentsViewModel { fun paginateCommentsError(): Observable fun pullToRefreshError(): Observable fun startThreadActivity(): Observable> + fun hasPendingComments(): Observable> /** Emits a boolean indicating whether comments are being fetched from the API. */ fun isFetchingComments(): Observable @@ -75,6 +78,7 @@ interface CommentsViewModel { private val nextPage = PublishSubject.create() private val onShowGuideLinesLinkClicked = PublishSubject.create() private val onReplyClicked = PublishSubject.create>() + private val checkIfThereAnyPendingComments = PublishSubject.create() private val closeCommentsPage = BehaviorSubject.create() private val currentUserAvatar = BehaviorSubject.create() @@ -92,6 +96,7 @@ interface CommentsViewModel { private val displayPaginationError = BehaviorSubject.create() private val commentToRefresh = PublishSubject.create() private val startThreadActivity = BehaviorSubject.create>() + private val hasPendingComments = BehaviorSubject.create>() // - Error observables to handle the 3 different use cases private val internalError = BehaviorSubject.create() @@ -219,6 +224,20 @@ interface CommentsViewModel { this.displayPaginationError.onNext(true) } + this.commentsList + .compose(takePairWhen(checkIfThereAnyPendingComments)) + .compose(bindToLifecycle()) + .subscribe { pair -> + this.hasPendingComments.onNext( + Pair( + pair.first.any { + it.commentCardState == CommentCardStatus.TRYING_TO_POST.commentCardStatus + }, + pair.second + ) + ) + } + this.backPressed .compose(bindToLifecycle()) .subscribe { this.closeCommentsPage.onNext(it) } @@ -416,7 +435,7 @@ interface CommentsViewModel { override fun onShowGuideLinesLinkClicked() = onShowGuideLinesLinkClicked.onNext(null) override fun refreshComment(comment: Comment) = this.commentToRefresh.onNext(comment) override fun onReplyClicked(comment: Comment, openKeyboard: Boolean) = onReplyClicked.onNext(Pair(comment, openKeyboard)) - + override fun checkIfThereAnyPendingComments(isBackAction: Boolean) = checkIfThereAnyPendingComments.onNext(isBackAction) // - Outputs override fun closeCommentsPage(): Observable = closeCommentsPage override fun currentUserAvatar(): Observable = currentUserAvatar @@ -435,5 +454,7 @@ interface CommentsViewModel { override fun startThreadActivity(): Observable> = this.startThreadActivity override fun isFetchingComments(): Observable = this.isFetchingComments + + override fun hasPendingComments(): Observable> = this.hasPendingComments } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f8a75399ea..19834b760c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,4 +82,8 @@ Replies View more replies + Your comment wasn\'t posted + You\'ll lose the comment if you leave this page. + Cancel + Leave page From 5cab25e5f85f70cb8fd7522d9738485ace49dbf6 Mon Sep 17 00:00:00 2001 From: hadia Date: Fri, 16 Jul 2021 18:51:13 +0200 Subject: [PATCH 3/9] Handle Pending comment dialog --- .../com/kickstarter/viewmodels/CommentsViewHolderViewModel.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/kickstarter/viewmodels/CommentsViewHolderViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/CommentsViewHolderViewModel.kt index 35a0ed73b5..be3c16f951 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/CommentsViewHolderViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/CommentsViewHolderViewModel.kt @@ -307,6 +307,8 @@ interface CommentsViewHolderViewModel { .compose(bindToLifecycle()) .subscribe { this.commentCardStatus.onNext(CommentCardStatus.COMMENT_FOR_LOGIN_BACKED_USERS) + this.postedSuccessfully.onNext(it) + if (it.isReply()) this.isReplyButtonVisible.onNext(false) } } From 1c6bb071b0a5afe09ea8f3c168dd70c41d0e72ed Mon Sep 17 00:00:00 2001 From: hadia Date: Mon, 19 Jul 2021 17:21:35 +0200 Subject: [PATCH 4/9] Handle empty title action --- .../libs/utils/extensions/ContextExt.kt | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/kickstarter/libs/utils/extensions/ContextExt.kt b/app/src/main/java/com/kickstarter/libs/utils/extensions/ContextExt.kt index 2dfc1c17f3..8b2998e36d 100644 --- a/app/src/main/java/com/kickstarter/libs/utils/extensions/ContextExt.kt +++ b/app/src/main/java/com/kickstarter/libs/utils/extensions/ContextExt.kt @@ -36,15 +36,20 @@ fun Context.showAlertDialog( setMessage(message) // add a button - setPositiveButton(positiveActionTitle) { dialog, _ -> - dialog.dismiss() - positiveAction?.invoke() + positiveActionTitle?.let { + setPositiveButton(positiveActionTitle) { dialog, _ -> + dialog.dismiss() + positiveAction?.invoke() + } } - setNegativeButton(negativeActionTitle) { dialog, _ -> - dialog.dismiss() - negativeAction?.invoke() + negativeActionTitle?.let { + setNegativeButton(negativeActionTitle) { dialog, _ -> + dialog.dismiss() + negativeAction?.invoke() + } } + setCancelable(isCancelable) } From b42b7247f43cd019bea798a66be9924d71c425e3 Mon Sep 17 00:00:00 2001 From: hadia Date: Mon, 19 Jul 2021 18:21:35 +0200 Subject: [PATCH 5/9] add test case for pull and back --- .../ui/activities/CommentsActivity.kt | 8 +- .../viewmodels/CommentsViewModelTest.kt | 185 ++++++++++++++++++ 2 files changed, 191 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt b/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt index efab161d79..216bc0b940 100644 --- a/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt +++ b/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt @@ -154,10 +154,12 @@ class CommentsActivity : getString(R.string.leave_page), false, positiveAction = { - executeActions(isBackAction) + if (!isBackAction) { + binding.commentsSwipeRefreshLayout.isRefreshing = false + } }, negativeAction = { - viewModel.inputs.backPressed() + executeActions(isBackAction) } ) } @@ -165,6 +167,8 @@ class CommentsActivity : fun executeActions(isBackAction: Boolean) { if (!isBackAction) { viewModel.inputs.refresh() + } else { + viewModel.inputs.backPressed() } } diff --git a/app/src/test/java/com/kickstarter/viewmodels/CommentsViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/CommentsViewModelTest.kt index a6751804e6..6f00160017 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/CommentsViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/CommentsViewModelTest.kt @@ -38,6 +38,7 @@ class CommentsViewModelTest : KSRobolectricTestCase() { private val shouldShowPaginatedCell = TestSubscriber.create() private val openCommentGuideLines = TestSubscriber() private val startThreadActivity = BehaviorSubject.create>() + private val hasPendingComments = TestSubscriber>() @Test fun testCommentsViewModel_whenUserLoggedInAndBacking_shouldShowEnabledComposer() { @@ -520,4 +521,188 @@ class CommentsViewModelTest : KSRobolectricTestCase() { assertTrue(newList[2].commentCardState == commentCardData2.commentCardState) } } + + @Test + fun testComments_PullToRefreshWithPendingComment() { + val currentUser = UserFactory.user() + .toBuilder() + .id(1) + .build() + + val comment1 = CommentFactory.commentToPostWithUser(currentUser).toBuilder().id(1).body("comment1").build() + val comment2 = CommentFactory.commentToPostWithUser(currentUser).toBuilder().id(2).body("comment2").build() + val newPostedComment = CommentFactory.commentToPostWithUser(currentUser).toBuilder().id(3).body("comment3").build() + + val commentEnvelope = CommentEnvelopeFactory.commentsEnvelope().toBuilder() + .comments(listOf(comment1, comment2)) + .build() + + val testScheduler = TestScheduler() + val env = environment().toBuilder().apolloClient(object : MockApolloClient() { + override fun getProjectComments(slug: String, cursor: String?, limit: Int): Observable { + return Observable.just(commentEnvelope) + } + }) + .currentUser(MockCurrentUser(currentUser)) + .scheduler(testScheduler) + .build() + + val commentCardData1 = CommentCardData.builder() + .comment(comment1) + .commentCardState(CommentCardStatus.COMMENT_FOR_LOGIN_BACKED_USERS.commentCardStatus) + .build() + val commentCardData2 = CommentCardData.builder() + .comment(comment2) + .commentCardState(CommentCardStatus.COMMENT_FOR_LOGIN_BACKED_USERS.commentCardStatus) + .build() + val commentCardData3 = CommentCardData.builder() + .comment(newPostedComment) + .commentCardState(CommentCardStatus.TRYING_TO_POST.commentCardStatus) + .build() + val commentCardData3Updated = CommentCardData.builder() + .comment(newPostedComment) + .commentCardState(CommentCardStatus.COMMENT_FOR_LOGIN_BACKED_USERS.commentCardStatus) + .build() + + val vm = CommentsViewModel.ViewModel(env) + vm.intent(Intent().putExtra(IntentKey.PROJECT, ProjectFactory.project())) + vm.outputs.commentsList().subscribe(commentsList) + vm.outputs.hasPendingComments().subscribe(hasPendingComments) + + commentsList.assertValueCount(1) + vm.outputs.commentsList().take(0).subscribe { + val newList = it + assertTrue(newList.size == 2) + assertTrue(newList[0].comment?.body() == commentCardData1.comment?.body()) + assertTrue(newList[0].commentCardState == commentCardData1.commentCardState) + + assertTrue(newList[1].comment?.body() == commentCardData2.comment?.body()) + assertTrue(newList[1].commentCardState == commentCardData2.commentCardState) + } + + vm.inputs.checkIfThereAnyPendingComments(false) + + this.hasPendingComments.assertValue(Pair(false, false)) + // - New posted comment with status "TRYING_TO_POST" + vm.inputs.insertNewCommentToList(newPostedComment.body(), DateTime.now()) + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + commentsList.assertValueCount(2) + vm.outputs.commentsList().take(1).subscribe { + val newList = it + assertTrue(newList.size == 3) + assertTrue(newList[0].comment?.body() == commentCardData3.comment?.body()) + assertTrue(newList[0].commentCardState == commentCardData3.commentCardState) + + assertTrue(newList[1].comment?.body() == commentCardData1.comment?.body()) + assertTrue(newList[1].commentCardState == commentCardData1.commentCardState) + + assertTrue(newList[2].comment?.body() == commentCardData2.comment?.body()) + assertTrue(newList[2].commentCardState == commentCardData2.commentCardState) + } + + vm.inputs.checkIfThereAnyPendingComments(false) + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + this.hasPendingComments.assertValues(Pair(false, false), Pair(true, false)) + + // - Check the status of the newly posted comment + vm.inputs.refreshComment(newPostedComment) + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + + // - Check Pull to refresh + vm.inputs.checkIfThereAnyPendingComments(false) + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + this.hasPendingComments.assertValues(Pair(false, false), Pair(true, false), Pair(false, false)) + } + + @Test + fun testComments_BackWithPendingComment() { + val currentUser = UserFactory.user() + .toBuilder() + .id(1) + .build() + + val comment1 = CommentFactory.commentToPostWithUser(currentUser).toBuilder().id(1).body("comment1").build() + val comment2 = CommentFactory.commentToPostWithUser(currentUser).toBuilder().id(2).body("comment2").build() + val newPostedComment = CommentFactory.commentToPostWithUser(currentUser).toBuilder().id(3).body("comment3").build() + + val commentEnvelope = CommentEnvelopeFactory.commentsEnvelope().toBuilder() + .comments(listOf(comment1, comment2)) + .build() + + val testScheduler = TestScheduler() + val env = environment().toBuilder().apolloClient(object : MockApolloClient() { + override fun getProjectComments(slug: String, cursor: String?, limit: Int): Observable { + return Observable.just(commentEnvelope) + } + }) + .currentUser(MockCurrentUser(currentUser)) + .scheduler(testScheduler) + .build() + + val commentCardData1 = CommentCardData.builder() + .comment(comment1) + .commentCardState(CommentCardStatus.COMMENT_FOR_LOGIN_BACKED_USERS.commentCardStatus) + .build() + val commentCardData2 = CommentCardData.builder() + .comment(comment2) + .commentCardState(CommentCardStatus.COMMENT_FOR_LOGIN_BACKED_USERS.commentCardStatus) + .build() + val commentCardData3 = CommentCardData.builder() + .comment(newPostedComment) + .commentCardState(CommentCardStatus.TRYING_TO_POST.commentCardStatus) + .build() + val commentCardData3Updated = CommentCardData.builder() + .comment(newPostedComment) + .commentCardState(CommentCardStatus.COMMENT_FOR_LOGIN_BACKED_USERS.commentCardStatus) + .build() + + val vm = CommentsViewModel.ViewModel(env) + vm.intent(Intent().putExtra(IntentKey.PROJECT, ProjectFactory.project())) + vm.outputs.commentsList().subscribe(commentsList) + vm.outputs.hasPendingComments().subscribe(hasPendingComments) + + commentsList.assertValueCount(1) + vm.outputs.commentsList().take(0).subscribe { + val newList = it + assertTrue(newList.size == 2) + assertTrue(newList[0].comment?.body() == commentCardData1.comment?.body()) + assertTrue(newList[0].commentCardState == commentCardData1.commentCardState) + + assertTrue(newList[1].comment?.body() == commentCardData2.comment?.body()) + assertTrue(newList[1].commentCardState == commentCardData2.commentCardState) + } + + vm.inputs.checkIfThereAnyPendingComments(true) + + this.hasPendingComments.assertValue(Pair(false, true)) + // - New posted comment with status "TRYING_TO_POST" + vm.inputs.insertNewCommentToList(newPostedComment.body(), DateTime.now()) + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + commentsList.assertValueCount(2) + vm.outputs.commentsList().take(1).subscribe { + val newList = it + assertTrue(newList.size == 3) + assertTrue(newList[0].comment?.body() == commentCardData3.comment?.body()) + assertTrue(newList[0].commentCardState == commentCardData3.commentCardState) + + assertTrue(newList[1].comment?.body() == commentCardData1.comment?.body()) + assertTrue(newList[1].commentCardState == commentCardData1.commentCardState) + + assertTrue(newList[2].comment?.body() == commentCardData2.comment?.body()) + assertTrue(newList[2].commentCardState == commentCardData2.commentCardState) + } + + vm.inputs.checkIfThereAnyPendingComments(true) + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + this.hasPendingComments.assertValues(Pair(false, true), Pair(true, true)) + + // - Check the status of the newly posted comment + vm.inputs.refreshComment(newPostedComment) + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + + // - Check Pull to refresh + vm.inputs.checkIfThereAnyPendingComments(true) + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + this.hasPendingComments.assertValues(Pair(false, true), Pair(true, true), Pair(false, true)) + } } From 873d944fdcd8f6db809bd2df1b8b3ecb4f5a2d14 Mon Sep 17 00:00:00 2001 From: hadia Date: Mon, 19 Jul 2021 20:29:42 +0200 Subject: [PATCH 6/9] Changes in bind failed state --- .../ui/activities/CommentsActivity.kt | 1 + .../viewmodels/CommentsViewModel.kt | 25 ++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt b/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt index 216bc0b940..eb2a58c38c 100644 --- a/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt +++ b/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt @@ -252,6 +252,7 @@ class CommentsActivity : } override fun onCommentPostedFailed(comment: Comment) { + viewModel.inputs.refreshCommentCardInCaseFailedPosted(comment) } override fun onCommentRepliesClicked(comment: Comment) { diff --git a/app/src/main/java/com/kickstarter/viewmodels/CommentsViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/CommentsViewModel.kt index fb5429b797..af3f113d4e 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/CommentsViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/CommentsViewModel.kt @@ -16,6 +16,7 @@ import com.kickstarter.models.Project import com.kickstarter.models.Update import com.kickstarter.models.User import com.kickstarter.models.extensions.updateCommentAfterSuccessfulPost +import com.kickstarter.models.extensions.updateCommentFailedToPost import com.kickstarter.services.ApiClientType import com.kickstarter.services.ApolloClientType import com.kickstarter.services.apiresponses.commentresponse.CommentEnvelope @@ -42,6 +43,7 @@ interface CommentsViewModel { /** Will be called with the successful response when calling the `postComment` Mutation **/ fun refreshComment(comment: Comment) + fun refreshCommentCardInCaseFailedPosted(comment: Comment) } interface Outputs { @@ -80,6 +82,7 @@ interface CommentsViewModel { private val onShowGuideLinesLinkClicked = PublishSubject.create() private val onReplyClicked = PublishSubject.create>() private val checkIfThereAnyPendingComments = PublishSubject.create() + private val failedCommentCardToRefresh = PublishSubject.create() private val closeCommentsPage = BehaviorSubject.create() private val currentUserAvatar = BehaviorSubject.create() @@ -232,7 +235,8 @@ interface CommentsViewModel { this.hasPendingComments.onNext( Pair( pair.first.any { - it.commentCardState == CommentCardStatus.TRYING_TO_POST.commentCardStatus + it.commentCardState == CommentCardStatus.TRYING_TO_POST.commentCardStatus || + it.commentCardState == CommentCardStatus.FAILED_TO_SEND_COMMENT.commentCardStatus }, pair.second ) @@ -260,10 +264,10 @@ interface CommentsViewModel { } // - Update internal mutable list with the latest state after successful response - this.commentToRefresh - .compose(combineLatestPair(this.commentsList)) + this.commentsList + .compose(takePairWhen(this.commentToRefresh)) .map { - it.first.updateCommentAfterSuccessfulPost(it.second) + it.second.updateCommentAfterSuccessfulPost(it.first) } .distinctUntilChanged() .compose(bindToLifecycle()) @@ -278,6 +282,17 @@ interface CommentsViewModel { .subscribe { this.outputCommentList.onNext(it) } + // - Update internal mutable list with the latest state after failed response + this.commentsList + .compose(takePairWhen(this.failedCommentCardToRefresh)) + .map { + it.second.updateCommentFailedToPost(it.first) + } + .distinctUntilChanged() + .compose(bindToLifecycle()) + .subscribe { + this.commentsList.onNext(it) + } } private fun loadCommentListFromProjectOrUpdate(projectOrUpdate: Observable>) { @@ -410,6 +425,8 @@ interface CommentsViewModel { override fun refreshComment(comment: Comment) = this.commentToRefresh.onNext(comment) override fun onReplyClicked(comment: Comment, openKeyboard: Boolean) = onReplyClicked.onNext(Pair(comment, openKeyboard)) override fun checkIfThereAnyPendingComments(isBackAction: Boolean) = checkIfThereAnyPendingComments.onNext(isBackAction) + override fun refreshCommentCardInCaseFailedPosted(comment: Comment) = + this.failedCommentCardToRefresh.onNext(comment) // - Outputs override fun closeCommentsPage(): Observable = closeCommentsPage override fun currentUserAvatar(): Observable = currentUserAvatar From 3be415bff2ca9d34d9d4b2f4b9d222b15e2ddaed Mon Sep 17 00:00:00 2001 From: hadia Date: Mon, 19 Jul 2021 20:42:11 +0200 Subject: [PATCH 7/9] Fix commit --- .../java/com/kickstarter/ui/activities/CommentsActivity.kt | 2 +- .../main/java/com/kickstarter/viewmodels/CommentsViewModel.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt b/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt index eb2a58c38c..9830430dd9 100644 --- a/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt +++ b/app/src/main/java/com/kickstarter/ui/activities/CommentsActivity.kt @@ -155,7 +155,7 @@ class CommentsActivity : false, positiveAction = { if (!isBackAction) { - binding.commentsSwipeRefreshLayout.isRefreshing = false + binding.commentsSwipeRefreshLayout.isRefreshing = false } }, negativeAction = { diff --git a/app/src/main/java/com/kickstarter/viewmodels/CommentsViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/CommentsViewModel.kt index af3f113d4e..2dd24442dd 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/CommentsViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/CommentsViewModel.kt @@ -236,7 +236,7 @@ interface CommentsViewModel { Pair( pair.first.any { it.commentCardState == CommentCardStatus.TRYING_TO_POST.commentCardStatus || - it.commentCardState == CommentCardStatus.FAILED_TO_SEND_COMMENT.commentCardStatus + it.commentCardState == CommentCardStatus.FAILED_TO_SEND_COMMENT.commentCardStatus }, pair.second ) @@ -286,7 +286,7 @@ interface CommentsViewModel { this.commentsList .compose(takePairWhen(this.failedCommentCardToRefresh)) .map { - it.second.updateCommentFailedToPost(it.first) + it.second.updateCommentFailedToPost(it.first) } .distinctUntilChanged() .compose(bindToLifecycle()) From 8b8e737cd661a49ac5e7862510d170b31be5a251 Mon Sep 17 00:00:00 2001 From: hadia Date: Mon, 19 Jul 2021 22:35:29 +0200 Subject: [PATCH 8/9] handling showing dialog on back --- .../ui/activities/ThreadActivity.kt | 54 ++++++- .../kickstarter/viewmodels/ThreadViewModel.kt | 41 +++++- .../viewmodels/ThreadViewModelTest.kt | 132 ++++++++++++++++++ 3 files changed, 216 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/kickstarter/ui/activities/ThreadActivity.kt b/app/src/main/java/com/kickstarter/ui/activities/ThreadActivity.kt index 381a8efe4f..7483074d34 100644 --- a/app/src/main/java/com/kickstarter/ui/activities/ThreadActivity.kt +++ b/app/src/main/java/com/kickstarter/ui/activities/ThreadActivity.kt @@ -13,6 +13,7 @@ import com.kickstarter.libs.RecyclerViewPaginator import com.kickstarter.libs.qualifiers.RequiresActivityViewModel import com.kickstarter.libs.utils.ApplicationUtils import com.kickstarter.libs.utils.UrlUtils +import com.kickstarter.libs.utils.extensions.showAlertDialog import com.kickstarter.models.Comment import com.kickstarter.ui.adapters.RepliesAdapter import com.kickstarter.ui.adapters.RepliesStatusAdapter @@ -71,13 +72,14 @@ class ThreadActivity : .observeOn(AndroidSchedulers.mainThread()) .doOnNext { linearLayoutManager.stackFromEnd = false - /** bind View more cell if the replies more than 7 or update after refresh initial error state **/ - this.repliesStatusAdapter.addViewMoreCell(it.second) } - .filter { it.first.isNotEmpty() } .subscribe { - /** bind replies list to adapter as reversed as the layout is reversed **/ - this.repliesAdapter.takeData(it.first.reversed()) + /** bind View more cell if the replies more than 7 or update after refresh initial error state **/ + this.repliesStatusAdapter.addViewMoreCell(it.second) + if (it.first.isNotEmpty()) { + /** bind replies list to adapter as reversed as the layout is reversed **/ + this.repliesAdapter.takeData(it.first.reversed()) + } } viewModel.outputs.shouldShowPaginationErrorUI() @@ -167,6 +169,48 @@ class ThreadActivity : ) ) } + + viewModel.outputs.hasPendingComments() + .compose(bindToLifecycle()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + if (it) { + handleBackAction() + } else { + viewModel.inputs.backPressed() + } + } + + viewModel.outputs.closeThreadActivity() + .compose(bindToLifecycle()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + closeCommentsActivity() + } + } + + private fun handleBackAction() { + this.showAlertDialog( + getString(R.string.Your_comment_wasnt_posted), + getString(R.string.You_will_lose_the_comment), + getString(R.string.cancel), + getString(R.string.leave_page), + false, + positiveAction = { + }, + negativeAction = { + viewModel.inputs.backPressed() + } + ) + } + + override fun back() { + viewModel.inputs.checkIfThereAnyPendingComments() + } + + private fun closeCommentsActivity() { + super.back() + this.finishActivity(taskId) } fun postReply(comment: String) { diff --git a/app/src/main/java/com/kickstarter/viewmodels/ThreadViewModel.kt b/app/src/main/java/com/kickstarter/viewmodels/ThreadViewModel.kt index ab48aea7ba..e08ef06e5a 100644 --- a/app/src/main/java/com/kickstarter/viewmodels/ThreadViewModel.kt +++ b/app/src/main/java/com/kickstarter/viewmodels/ThreadViewModel.kt @@ -36,6 +36,8 @@ interface ThreadViewModel { fun onShowGuideLinesLinkClicked() fun refreshCommentCardInCaseFailedPosted(comment: Comment) fun refreshCommentCardInCaseSuccessPosted(comment: Comment) + fun checkIfThereAnyPendingComments() + fun backPressed() } interface Outputs { @@ -64,6 +66,8 @@ interface ThreadViewModel { fun refresh(): Observable fun showCommentGuideLinesLink(): Observable + fun hasPendingComments(): Observable + fun closeThreadActivity(): Observable } class ViewModel(@NonNull val environment: Environment) : ActivityViewModel(environment), Inputs, Outputs { @@ -76,6 +80,8 @@ interface ThreadViewModel { private val onShowGuideLinesLinkClicked = PublishSubject.create() private val failedCommentCardToRefresh = PublishSubject.create() private val successfullyPostedCommentCardToRefresh = PublishSubject.create() + private val checkIfThereAnyPendingComments = PublishSubject.create() + private val backPressed = PublishSubject.create() private val rootComment = BehaviorSubject.create() private val focusOnCompose = BehaviorSubject.create() @@ -90,6 +96,8 @@ interface ThreadViewModel { private val displayPaginationError = BehaviorSubject.create() private val initialLoadCommentsError = BehaviorSubject.create() private val showGuideLinesLink = BehaviorSubject.create() + private val hasPendingComments = BehaviorSubject.create() + private val closeThreadActivity = BehaviorSubject.create() private val onCommentReplies = BehaviorSubject.create, Boolean>>() @@ -228,10 +236,10 @@ interface ThreadViewModel { } // - Update internal mutable list with the latest state after failed response - this.failedCommentCardToRefresh - .compose(Transformers.combineLatestPair(this.onCommentReplies)) + this.onCommentReplies + .compose(Transformers.combineLatestPair(this.failedCommentCardToRefresh)) .map { - Pair(it.first.updateCommentFailedToPost(it.second.first), it.second.second) + Pair(it.second.updateCommentFailedToPost(it.first.first), it.first.second) } .distinctUntilChanged() .compose(bindToLifecycle()) @@ -240,16 +248,32 @@ interface ThreadViewModel { } // - Update internal mutable list with the latest state after successful response - this.successfullyPostedCommentCardToRefresh - .compose(Transformers.combineLatestPair(this.onCommentReplies)) + this.onCommentReplies + .compose(Transformers.combineLatestPair(this.successfullyPostedCommentCardToRefresh)) .map { - Pair(it.first.updateCommentAfterSuccessfulPost(it.second.first), it.second.second) + Pair(it.second.updateCommentAfterSuccessfulPost(it.first.first), it.first.second) } .distinctUntilChanged() .compose(bindToLifecycle()) .subscribe { this.onCommentReplies.onNext(it) } + + this.onCommentReplies + .compose(Transformers.takePairWhen(checkIfThereAnyPendingComments)) + .compose(bindToLifecycle()) + .subscribe { pair -> + this.hasPendingComments.onNext( + pair.first.first.any { + it.commentCardState == CommentCardStatus.TRYING_TO_POST.commentCardStatus || + it.commentCardState == CommentCardStatus.FAILED_TO_SEND_COMMENT.commentCardStatus + } + ) + } + + this.backPressed + .compose(bindToLifecycle()) + .subscribe { this.closeThreadActivity.onNext(it) } } private fun loadCommentListFromProjectOrUpdate(comment: Observable) { @@ -382,8 +406,13 @@ interface ThreadViewModel { override fun isFetchingReplies(): Observable = this.isFetchingReplies override fun loadMoreReplies(): Observable = this.loadMoreReplies override fun showCommentGuideLinesLink(): Observable = showGuideLinesLink + override fun checkIfThereAnyPendingComments() = checkIfThereAnyPendingComments.onNext(null) + override fun backPressed() = backPressed.onNext(null) + override fun shouldShowPaginationErrorUI(): Observable = this.displayPaginationError override fun initialLoadCommentsError(): Observable = this.initialLoadCommentsError override fun refresh(): Observable = this.refresh + override fun hasPendingComments(): Observable = this.hasPendingComments + override fun closeThreadActivity(): Observable = this.closeThreadActivity } } diff --git a/app/src/test/java/com/kickstarter/viewmodels/ThreadViewModelTest.kt b/app/src/test/java/com/kickstarter/viewmodels/ThreadViewModelTest.kt index c04edafecc..c7071ae064 100644 --- a/app/src/test/java/com/kickstarter/viewmodels/ThreadViewModelTest.kt +++ b/app/src/test/java/com/kickstarter/viewmodels/ThreadViewModelTest.kt @@ -39,6 +39,8 @@ class ThreadViewModelTest : KSRobolectricTestCase() { private val loadMoreReplies = TestSubscriber() private val openCommentGuideLines = TestSubscriber() private val refresh = TestSubscriber() + private val hasPendingComments = TestSubscriber() + private val closeThreadActivity = TestSubscriber() private fun setUpEnvironment() { setUpEnvironment(environment()) @@ -516,4 +518,134 @@ class ThreadViewModelTest : KSRobolectricTestCase() { assertTrue(newList[2].commentCardState == commentCardData2.commentCardState) } } + + @Test + fun backButtonPressed_whenEmits_shouldEmitToCloseActivityStream() { + setUpEnvironment() + vm.outputs.closeThreadActivity().subscribe(closeThreadActivity) + + vm.inputs.backPressed() + closeThreadActivity.assertValueCount(1) + } + + @Test + fun testReplies_BackWithPendingComment() { + val currentUser = UserFactory.user() + .toBuilder() + .id(1) + .build() + + val comment1 = CommentFactory.commentToPostWithUser(currentUser).toBuilder().id(1).body("comment1").build() + val comment2 = CommentFactory.commentToPostWithUser(currentUser).toBuilder().id(2).body("comment2").build() + val newPostedComment = CommentFactory.commentToPostWithUser(currentUser).toBuilder().id(3).body("comment3").build() + + val commentEnvelope = CommentEnvelopeFactory.commentsEnvelope().toBuilder() + .comments(listOf(comment1, comment2)) + .build() + + val testScheduler = TestScheduler() + + val env = environment().toBuilder() + .apolloClient(object : MockApolloClient() { + override fun getRepliesForComment( + comment: Comment, + cursor: String?, + pageSize: Int + ): Observable { + return Observable.just(commentEnvelope) + } + }) + .currentUser(MockCurrentUser(currentUser)) + .scheduler(testScheduler) + .build() + + val commentCardData1 = CommentCardData.builder() + .comment(comment1) + .commentCardState(CommentCardStatus.COMMENT_FOR_LOGIN_BACKED_USERS.commentCardStatus) + .build() + val commentCardData2 = CommentCardData.builder() + .comment(comment2) + .commentCardState(CommentCardStatus.COMMENT_FOR_LOGIN_BACKED_USERS.commentCardStatus) + .build() + val commentCardData3 = CommentCardData.builder() + .comment(newPostedComment) + .commentCardState(CommentCardStatus.TRYING_TO_POST.commentCardStatus) + .build() + + val commentCardData3Failed = CommentCardData.builder() + .comment(newPostedComment) + .commentCardState(CommentCardStatus.FAILED_TO_SEND_COMMENT.commentCardStatus) + .build() + + val commentCardData3Updated = CommentCardData.builder() + .comment(newPostedComment) + .commentCardState(CommentCardStatus.COMMENT_FOR_LOGIN_BACKED_USERS.commentCardStatus) + .build() + + val vm = ThreadViewModel.ViewModel(env) + // Start the view model with a backed project and comment. + vm.intent(Intent().putExtra(IntentKey.COMMENT_CARD_DATA, CommentCardDataFactory.commentCardData())) + vm.outputs.onCommentReplies().subscribe(onReplies) + + vm.outputs.onCommentReplies().subscribe() + vm.outputs.hasPendingComments().subscribe(hasPendingComments) + + onReplies.assertValueCount(1) + vm.outputs.onCommentReplies().take(0).subscribe { + val newList = it.first + assertTrue(newList.size == 2) + assertTrue(newList[0].comment?.body() == commentCardData1.comment?.body()) + assertTrue(newList[0].commentCardState == commentCardData1.commentCardState) + + assertTrue(newList[1].comment?.body() == commentCardData2.comment?.body()) + assertTrue(newList[1].commentCardState == commentCardData2.commentCardState) + } + + vm.inputs.checkIfThereAnyPendingComments() + + this.hasPendingComments.assertValue(false) + // - New posted comment with status "TRYING_TO_POST" + vm.inputs.insertNewReplyToList(newPostedComment.body(), DateTime.now()) + testScheduler.advanceTimeBy(3, TimeUnit.SECONDS) + onReplies.assertValueCount(2) + vm.outputs.onCommentReplies().take(0).subscribe { + val newList = it.first + assertTrue(newList.size == 3) + assertTrue(newList[0].comment?.body() == commentCardData3.comment?.body()) + assertTrue(newList[0].commentCardState == commentCardData3.commentCardState) + + assertTrue(newList[1].comment?.body() == commentCardData1.comment?.body()) + assertTrue(newList[1].commentCardState == commentCardData1.commentCardState) + + assertTrue(newList[2].comment?.body() == commentCardData2.comment?.body()) + assertTrue(newList[2].commentCardState == commentCardData2.commentCardState) + } + + vm.inputs.checkIfThereAnyPendingComments() + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + this.hasPendingComments.assertValues(false, true) + + // - Check the status of the newly posted comment + vm.inputs.refreshCommentCardInCaseFailedPosted(newPostedComment) + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + + onReplies.assertValueCount(3) + vm.outputs.onCommentReplies().take(0).subscribe { + val newList = it.first + assertTrue(newList.size == 3) + assertTrue(newList[0].comment?.body() == commentCardData3Failed.comment?.body()) + assertTrue(newList[0].commentCardState == commentCardData3Failed.commentCardState) + + assertTrue(newList[1].comment?.body() == commentCardData1.comment?.body()) + assertTrue(newList[1].commentCardState == commentCardData1.commentCardState) + + assertTrue(newList[2].comment?.body() == commentCardData2.comment?.body()) + assertTrue(newList[2].commentCardState == commentCardData2.commentCardState) + } + + // - Check Pull to refresh + vm.inputs.checkIfThereAnyPendingComments() + testScheduler.advanceTimeBy(2, TimeUnit.SECONDS) + this.hasPendingComments.assertValues(false, true, true) + } } From 29debffcd2a3de4c4d1fa61e4c4c67bda3526fb5 Mon Sep 17 00:00:00 2001 From: hadia Date: Mon, 19 Jul 2021 23:28:56 +0200 Subject: [PATCH 9/9] handling showing dialog on back --- .../java/com/kickstarter/ui/activities/ThreadActivity.kt | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/app/src/main/java/com/kickstarter/ui/activities/ThreadActivity.kt b/app/src/main/java/com/kickstarter/ui/activities/ThreadActivity.kt index 7483074d34..06d4877ad5 100644 --- a/app/src/main/java/com/kickstarter/ui/activities/ThreadActivity.kt +++ b/app/src/main/java/com/kickstarter/ui/activities/ThreadActivity.kt @@ -174,11 +174,7 @@ class ThreadActivity : .compose(bindToLifecycle()) .observeOn(AndroidSchedulers.mainThread()) .subscribe { - if (it) { - handleBackAction() - } else { - viewModel.inputs.backPressed() - } + if (it) handleBackAction() else viewModel.inputs.backPressed() } viewModel.outputs.closeThreadActivity()