Skip to content

Commit

Permalink
NT-1951: UI/UX – Error loading initial comments (#1284)
Browse files Browse the repository at this point in the history
* added error layout

* added custom error

* refactored comments viewmodel

* fixed tests

* fixed lint errors

* fixed failing tests

* refactored pull refresh errors

* fixed lint errors

* revome redundant viewholder

* revome redundant viewholder

Co-authored-by: Isabel Martin <arkariang@gmail.com>
  • Loading branch information
sunday-okpoluaefe and Arkariang committed Jun 11, 2021
1 parent 4f9dd71 commit 3737f82
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 32 deletions.
@@ -0,0 +1,10 @@
package com.kickstarter.libs.utils.extensions

import android.view.View

fun Boolean.toVisibility(): Int {
return when (this) {
true -> View.VISIBLE
else -> View.GONE
}
}
Expand Up @@ -11,6 +11,7 @@ import com.kickstarter.libs.loadmore.PaginationHandler
import com.kickstarter.libs.qualifiers.RequiresActivityViewModel
import com.kickstarter.libs.utils.ApplicationUtils
import com.kickstarter.libs.utils.UrlUtils
import com.kickstarter.libs.utils.extensions.toVisibility
import com.kickstarter.models.Comment
import com.kickstarter.ui.IntentKey
import com.kickstarter.ui.adapters.CommentsAdapter
Expand Down Expand Up @@ -74,6 +75,13 @@ class CommentsActivity :
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::setEmptyState)

viewModel.outputs.initialLoadCommentsError()
.compose(bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
adapter.insertPageError()
}

/*
* A little delay after new item is inserted
* This is necessary for the scroll to take effect
Expand Down Expand Up @@ -164,6 +172,8 @@ class CommentsActivity :
true -> View.VISIBLE
else -> View.GONE
}
binding.commentsSwipeRefreshLayout.visibility = (!visibility).toVisibility()
binding.noComments.visibility = visibility.toVisibility()
}

override fun emptyCommentsLoginClicked(viewHolder: EmptyCommentsViewHolder?) {
Expand Down
43 changes: 39 additions & 4 deletions app/src/main/java/com/kickstarter/ui/adapters/CommentsAdapter.kt
Expand Up @@ -4,27 +4,62 @@ import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import com.kickstarter.R
import com.kickstarter.databinding.CommentInitialLoadErrorLayoutBinding
import com.kickstarter.databinding.ItemCommentCardBinding
import com.kickstarter.ui.data.CommentCardData
import com.kickstarter.ui.viewholders.CommentCardViewHolder
import com.kickstarter.ui.viewholders.EmptyCommentsViewHolder
import com.kickstarter.ui.viewholders.EmptyViewHolder
import com.kickstarter.ui.viewholders.KSViewHolder

class CommentsAdapter(private val delegate: Delegate) : KSListAdapter() {
interface Delegate : EmptyCommentsViewHolder.Delegate, CommentCardViewHolder.Delegate

companion object {
private const val SECTION_INITIAL_LOAD_ERROR = 0
private const val SECTION_COMMENT_CARD = 1
}

init {
resetList()
}

@LayoutRes
override fun layout(sectionRow: SectionRow): Int {
return R.layout.item_comment_card
return when (sectionRow.section()) {
SECTION_COMMENT_CARD -> R.layout.item_comment_card
SECTION_INITIAL_LOAD_ERROR -> R.layout.comment_initial_load_error_layout
else -> 0
}
}

fun takeData(comments: List<CommentCardData>) {
private fun resetList() {
clearSections()
addSection(comments)
insertSection(SECTION_INITIAL_LOAD_ERROR, emptyList<Boolean>())
insertSection(SECTION_COMMENT_CARD, emptyList<CommentCardData>())
}

fun takeData(comments: List<CommentCardData>) {
resetList()
setSection(SECTION_COMMENT_CARD, comments)
submitList(items())
}

fun insertPageError() {
resetList()
setSection(SECTION_INITIAL_LOAD_ERROR, listOf(true))
submitList(items())
}

override fun viewHolder(@LayoutRes layout: Int, viewGroup: ViewGroup): KSViewHolder {
return CommentCardViewHolder(ItemCommentCardBinding.inflate(LayoutInflater.from(viewGroup.context), viewGroup, false), delegate)
return when (layout) {
R.layout.item_comment_card -> CommentCardViewHolder(ItemCommentCardBinding.inflate(LayoutInflater.from(viewGroup.context), viewGroup, false), delegate)
R.layout.comment_initial_load_error_layout -> EmptyViewHolder(
CommentInitialLoadErrorLayoutBinding.inflate(LayoutInflater.from(viewGroup.context), viewGroup, false)
)
else -> EmptyViewHolder(
CommentInitialLoadErrorLayoutBinding.inflate(LayoutInflater.from(viewGroup.context), viewGroup, false)
)
}
}
}
70 changes: 46 additions & 24 deletions app/src/main/java/com/kickstarter/viewmodels/CommentsViewModel.kt
Expand Up @@ -28,7 +28,6 @@ import org.joda.time.DateTime
import rx.Observable
import rx.subjects.BehaviorSubject
import rx.subjects.PublishSubject
import timber.log.Timber

interface CommentsViewModel {

Expand Down Expand Up @@ -88,9 +87,17 @@ interface CommentsViewModel {
private val paginationError = BehaviorSubject.create<Throwable>()
private val pullToRefreshError = BehaviorSubject.create<Throwable>()

private val isFetchingData = BehaviorSubject.create<Int>()

private var lastCommentCursor: String? = null
override var loadMoreListData = mutableListOf<CommentCardData>()

companion object {
private const val INITIAL_LOAD = 1
private const val PULL_LOAD = 2
private const val PAGE_LOAD = 3
}

init {

val loggedInUser = this.currentUser.loggedInUser()
Expand Down Expand Up @@ -182,22 +189,34 @@ interface CommentsViewModel {
this.setEmptyState.onNext(it == 0)
}

// TODO showcasing subscription to initialization to be completed on : https://kickstarter.atlassian.net/browse/NT-1951
this.internalError
.compose(combineLatestPair(commentsList))
.filter { it.second.isEmpty() }
.map { it.first }
.compose(combineLatestPair(isFetchingData))
.filter {
this.lastCommentCursor == null &&
it.second == INITIAL_LOAD
}
.compose(bindToLifecycle())
.subscribe {
it.localizedMessage
Timber.d("************ On initializing error")
this.initialError.onNext(it)
this.initialError.onNext(it.first)
}

// TODO showcasing pagination error subscriptionto be completed on : https://kickstarter.atlassian.net/browse/NT-2019
this.paginationError
this.internalError
.filter { this.lastCommentCursor != null }
.compose(bindToLifecycle())
.subscribe {
it.localizedMessage
Timber.d("************ On pagination error")
this.paginationError.onNext(it)
}

this.internalError
.compose(combineLatestPair(isFetchingData))
.filter {
this.lastCommentCursor == null &&
it.second == PULL_LOAD
}
.compose(bindToLifecycle())
.subscribe {
it.first.localizedMessage
this.isRefreshing.onNext(false)
}

this.backPressed
Expand All @@ -206,9 +225,8 @@ interface CommentsViewModel {
}

private fun loadCommentList(initialProject: Observable<Project>) {

// - First load for comments & handle initial load errors
getProjectComments(initialProject, this.internalError)
getProjectComments(initialProject, INITIAL_LOAD)
.compose(bindToLifecycle())
.subscribe {
bindCommentList(it.first, LoadingType.NORMAL)
Expand All @@ -220,7 +238,7 @@ interface CommentsViewModel {
.doOnNext {
this.isLoadingMoreItems.onNext(true)
}
.switchMap { getProjectComments(Observable.just(it), this.paginationError) }
.switchMap { getProjectComments(Observable.just(it), PAGE_LOAD) }
.compose(bindToLifecycle())
.subscribe {
updatePaginatedData(
Expand All @@ -240,22 +258,26 @@ interface CommentsViewModel {
lastCommentCursor = null
this.loadMoreListData.clear()
}
.switchMap { getProjectComments(Observable.just(it), this.pullToRefreshError) }
.switchMap { getProjectComments(Observable.just(it), PULL_LOAD) }
.compose(bindToLifecycle())
.subscribe {
bindCommentList(it.first, LoadingType.PULL_REFRESH)
}
}

private fun getProjectComments(project: Observable<Project>, errorObservable: BehaviorSubject<Throwable>) = project.switchMap {
return@switchMap apolloClient.getProjectComments(it.slug() ?: "", lastCommentCursor)
}.doOnError {
errorObservable.onNext(it)
private fun getProjectComments(project: Observable<Project>, state: Int): Observable<Pair<List<CommentCardData>, Int>> {
isFetchingData.onNext(state)
return project.switchMap {
return@switchMap apolloClient.getProjectComments(it.slug() ?: "", lastCommentCursor)
}.doOnError {
this.internalError.onNext(it)
this.isLoadingMoreItems.onNext(false)
}
.onErrorResumeNext(Observable.empty())
.filter { ObjectUtils.isNotNull(it) }
.compose<Pair<CommentEnvelope, Project>>(combineLatestPair(project))
.map { Pair(requireNotNull(mapToCommentCardDataList(it)), it.first.totalCount) }
}
.onErrorResumeNext(Observable.empty())
.filter { ObjectUtils.isNotNull(it) }
.compose<Pair<CommentEnvelope, Project>>(combineLatestPair(project))
.map { Pair(requireNotNull(mapToCommentCardDataList(it)), it.first.totalCount) }

private fun mapToCommentCardDataList(it: Pair<CommentEnvelope, Project>) =
it.first.comments?.map { comment: Comment ->
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/layout/activity_comments_layout.xml
Expand Up @@ -93,4 +93,5 @@
app:layout_constraintStart_toStartOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

</androidx.coordinatorlayout.widget.CoordinatorLayout>
23 changes: 23 additions & 0 deletions app/src/main/res/layout/comment_initial_load_error_layout.xml
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_height="match_parent">

<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/error_loading_comment"
android:visibility="visible"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:gravity="center"
android:drawablePadding="@dimen/grid_2"
android:drawableTop="@drawable/ic_info"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:text="@string/something_went_wrong"
style="@style/CalloutSecondary"
android:layout_height="wrap_content"/>

</androidx.constraintlayout.widget.ConstraintLayout>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Expand Up @@ -84,6 +84,7 @@
<string name="leave_comment">Leave a comment…</string>
<string name="only_backers_can_leave_comments">Only backers can leave comments</string>
<!-- comment card -->
<string name="something_went_wrong">Something went wrong. Pull \ndown to reload this page.</string>
<string name="comment_has_been_removed">This comment has been removed by Kickstarter.<u>Learn more about comment guidelines</u> </string>
<string name="learn_more_about_comment_guidelines">Learn more about comment guidelines</string>
<string name="failed_to_post_retry">Failed to post. Tap to retry</string>
Expand Down
Expand Up @@ -250,7 +250,7 @@ class CommentsViewModelTest : KSRobolectricTestCase() {
}

@Test
fun testCommentsViewModel_ProjectRefresh_withError() {
fun testCommentsViewModel_ProjectRefresh_AndInitialLoad_withError() {
val env = environment().toBuilder().apolloClient(object : MockApolloClient() {
override fun getProjectComments(slug: String, cursor: String?, limit: Int): Observable<CommentEnvelope> {
return Observable.error(ApiExceptionFactory.badRequestException())
Expand All @@ -269,10 +269,9 @@ class CommentsViewModelTest : KSRobolectricTestCase() {
vm.outputs.commentsList().subscribe(commentsList)

// Comments should emit.
isRefreshing.assertValues(true)
isRefreshing.assertValues(true, false, false)
commentsList.assertValueCount(0)
pullToRefreshError.assertValueCount(1)
initialLoadError.assertValueCount(0)
initialLoadError.assertValueCount(1)
paginationError.assertNoValues()
}

Expand Down

0 comments on commit 3737f82

Please sign in to comment.