Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NT-2001:UX – Show pending reply dialog #1330

Merged
merged 17 commits into from
Jul 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 45 additions & 5 deletions app/src/main/java/com/kickstarter/ui/activities/ThreadActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -167,6 +169,44 @@ 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) {
Expand Down
41 changes: 35 additions & 6 deletions app/src/main/java/com/kickstarter/viewmodels/ThreadViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ interface ThreadViewModel {
fun onShowGuideLinesLinkClicked()
fun refreshCommentCardInCaseFailedPosted(comment: Comment)
fun refreshCommentCardInCaseSuccessPosted(comment: Comment)
fun checkIfThereAnyPendingComments()
fun backPressed()
}

interface Outputs {
Expand Down Expand Up @@ -64,6 +66,8 @@ interface ThreadViewModel {
fun refresh(): Observable<Void>

fun showCommentGuideLinesLink(): Observable<Void>
fun hasPendingComments(): Observable<Boolean>
fun closeThreadActivity(): Observable<Void>
}

class ViewModel(@NonNull val environment: Environment) : ActivityViewModel<ThreadActivity>(environment), Inputs, Outputs {
Expand All @@ -76,6 +80,8 @@ interface ThreadViewModel {
private val onShowGuideLinesLinkClicked = PublishSubject.create<Void>()
private val failedCommentCardToRefresh = PublishSubject.create<Comment>()
private val successfullyPostedCommentCardToRefresh = PublishSubject.create<Comment>()
private val checkIfThereAnyPendingComments = PublishSubject.create<Void>()
private val backPressed = PublishSubject.create<Void>()

private val rootComment = BehaviorSubject.create<Comment>()
private val focusOnCompose = BehaviorSubject.create<Boolean>()
Expand All @@ -90,6 +96,8 @@ interface ThreadViewModel {
private val displayPaginationError = BehaviorSubject.create<Boolean>()
private val initialLoadCommentsError = BehaviorSubject.create<Boolean>()
private val showGuideLinesLink = BehaviorSubject.create<Void>()
private val hasPendingComments = BehaviorSubject.create<Boolean>()
private val closeThreadActivity = BehaviorSubject.create<Void>()

private val onCommentReplies =
BehaviorSubject.create<Pair<List<CommentCardData>, Boolean>>()
Expand Down Expand Up @@ -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())
Expand All @@ -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<Comment>) {
Expand Down Expand Up @@ -382,8 +406,13 @@ interface ThreadViewModel {
override fun isFetchingReplies(): Observable<Boolean> = this.isFetchingReplies
override fun loadMoreReplies(): Observable<Void> = this.loadMoreReplies
override fun showCommentGuideLinesLink(): Observable<Void> = showGuideLinesLink
override fun checkIfThereAnyPendingComments() = checkIfThereAnyPendingComments.onNext(null)
override fun backPressed() = backPressed.onNext(null)

override fun shouldShowPaginationErrorUI(): Observable<Boolean> = this.displayPaginationError
override fun initialLoadCommentsError(): Observable<Boolean> = this.initialLoadCommentsError
override fun refresh(): Observable<Void> = this.refresh
override fun hasPendingComments(): Observable<Boolean> = this.hasPendingComments
override fun closeThreadActivity(): Observable<Void> = this.closeThreadActivity
}
}
132 changes: 132 additions & 0 deletions app/src/test/java/com/kickstarter/viewmodels/ThreadViewModelTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class ThreadViewModelTest : KSRobolectricTestCase() {
private val loadMoreReplies = TestSubscriber<Void>()
private val openCommentGuideLines = TestSubscriber<Void>()
private val refresh = TestSubscriber<Void>()
private val hasPendingComments = TestSubscriber<Boolean>()
private val closeThreadActivity = TestSubscriber<Void>()

private fun setUpEnvironment() {
setUpEnvironment(environment())
Expand Down Expand Up @@ -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<CommentEnvelope> {
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)
}
}