Skip to content

Commit

Permalink
NT-1958: UX – Post a reply (#1305)
Browse files Browse the repository at this point in the history
* refactored thread view model

* set scrollveiw scroll to bottom

* added unit tests

* fixed lint error

* added comment extension

* added comment extension

* removed position

* fixed lint errors

Co-authored-by: Hadia <hadiamohamed.iti@gmail.com>
  • Loading branch information
sunday-okpoluaefe and hadia committed Jul 2, 2021
1 parent 73ebff4 commit f30330b
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.kickstarter.libs.utils.extensions

import com.kickstarter.models.Comment

fun Comment.isReply() = this.parentId() > 0
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ class CommentsActivity :
startActivityWithTransition(
threadIntent,
R.anim.slide_in_right,
R.anim.fade_out_slide_out_left
R.anim.slide_out_right
)
}

Expand Down
18 changes: 16 additions & 2 deletions app/src/main/java/com/kickstarter/ui/activities/ThreadActivity.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.kickstarter.ui.activities

import android.os.Bundle
import android.view.View
import androidx.core.view.isVisible
import com.kickstarter.databinding.ActivityThreadLayoutBinding
import com.kickstarter.libs.BaseActivity
Expand All @@ -12,6 +13,7 @@ import com.kickstarter.ui.adapters.RepliesAdapter
import com.kickstarter.ui.extensions.hideKeyboard
import com.kickstarter.ui.views.OnCommentComposerViewClickedListener
import com.kickstarter.viewmodels.ThreadViewModel
import org.joda.time.DateTime
import rx.android.schedulers.AndroidSchedulers
import java.util.concurrent.TimeUnit

Expand Down Expand Up @@ -70,6 +72,14 @@ class ThreadActivity :
binding.replyComposer.setCommentComposerStatus(it)
}

viewModel.outputs.scrollToBottom()
.compose(bindToLifecycle())
.delay(200, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
binding.parentScrollContainer.fullScroll(View.FOCUS_DOWN)
}

viewModel.outputs.showReplyComposer()
.compose(bindToLifecycle())
.observeOn(AndroidSchedulers.mainThread())
Expand All @@ -80,12 +90,17 @@ class ThreadActivity :
binding.replyComposer.setCommentComposerActionClickListener(object :
OnCommentComposerViewClickedListener {
override fun onClickActionListener(string: String) {
// TODO add Post Replay
postReply(string)
hideKeyboard()
}
})
}

fun postReply(comment: String) {
this.viewModel.inputs.insertNewReplyToList(comment, DateTime.now())
this.binding.replyComposer.clearCommentComposer()
}

override fun onStop() {
super.onStop()
hideKeyboard()
Expand Down Expand Up @@ -121,6 +136,5 @@ class ThreadActivity :
}

override fun onCommentPostedSuccessFully(comment: Comment) {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ import com.kickstarter.ui.viewholders.KSViewHolder
class RepliesAdapter(private val delegate: Delegate) : KSListAdapter() {
interface Delegate : CommentCardViewHolder.Delegate

init {
insertSection(SECTION_REPLIES, emptyList<CommentCardData>())
}
fun takeData(replies: List<CommentCardData>) {
addSection(replies)
setSection(SECTION_REPLIES, replies)
submitList(items())
}

Expand All @@ -23,4 +26,8 @@ class RepliesAdapter(private val delegate: Delegate) : KSListAdapter() {
override fun viewHolder(layout: Int, view: ViewGroup): KSViewHolder {
return CommentCardViewHolder(ItemCommentCardBinding.inflate(LayoutInflater.from(view.context), view, false), delegate)
}

companion object {
private const val SECTION_REPLIES = 0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.kickstarter.libs.rx.transformers.Transformers.combineLatestPair
import com.kickstarter.libs.rx.transformers.Transformers.takeWhen
import com.kickstarter.libs.utils.ObjectUtils
import com.kickstarter.libs.utils.ProjectUtils
import com.kickstarter.libs.utils.extensions.isReply
import com.kickstarter.models.Comment
import com.kickstarter.models.Project
import com.kickstarter.models.User
Expand Down Expand Up @@ -288,6 +289,7 @@ interface CommentsViewHolderViewModel {
.subscribe {
this.commentCardStatus.onNext(CommentCardStatus.COMMENT_FOR_LOGIN_BACKED_USERS)
this.postedSuccessfully.onNext(it)
if (it.isReply()) this.isReplyButtonVisible.onNext(false)
}

Observable
Expand Down Expand Up @@ -375,7 +377,7 @@ interface CommentsViewHolderViewModel {
*/
private fun cardStatus(commentCardData: CommentCardData) = when {
commentCardData.comment?.deleted() ?: false -> CommentCardStatus.DELETED_COMMENT
(commentCardData.comment?.repliesCount() ?: false != 0) -> CommentCardStatus.COMMENT_WITH_REPLIES
(commentCardData.comment?.repliesCount() ?: 0 != 0) -> CommentCardStatus.COMMENT_WITH_REPLIES
else -> CommentCardStatus.values().firstOrNull { it.commentCardStatus == commentCardData.commentCardState }
}.also {
this.isCommentEnableThreads.onNext(optimizely.isFeatureEnabled(OptimizelyFeature.Key.COMMENT_ENABLE_THREADS))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,6 @@ interface CommentsViewModel {
)
}
.doOnNext { scrollToTop.onNext(true) }

.withLatestFrom(this.commentsList) { it, list ->
list.toMutableList().apply {
add(0, it.second)
Expand Down
61 changes: 60 additions & 1 deletion app/src/main/java/com/kickstarter/viewmodels/ThreadViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,19 @@ import com.kickstarter.services.apiresponses.commentresponse.CommentEnvelope
import com.kickstarter.ui.IntentKey
import com.kickstarter.ui.activities.ThreadActivity
import com.kickstarter.ui.data.CommentCardData
import com.kickstarter.ui.views.CommentCardStatus
import com.kickstarter.ui.views.CommentComposerStatus
import org.joda.time.DateTime
import rx.Observable
import rx.subjects.BehaviorSubject
import rx.subjects.PublishSubject

interface ThreadViewModel {

interface Inputs
interface Inputs {
fun insertNewReplyToList(comment: String, createdAt: DateTime)
}

interface Outputs {
/** The anchored root comment */
fun getRootComment(): Observable<Comment>
Expand All @@ -33,6 +39,7 @@ interface ThreadViewModel {

/** Will tell to the compose view if should open the keyboard */
fun shouldFocusOnCompose(): Observable<Boolean>
fun scrollToBottom(): Observable<Void>

fun currentUserAvatar(): Observable<String?>
fun replyComposerStatus(): Observable<CommentComposerStatus>
Expand All @@ -48,6 +55,8 @@ interface ThreadViewModel {
private val currentUserAvatar = BehaviorSubject.create<String?>()
private val replyComposerStatus = BehaviorSubject.create<CommentComposerStatus>()
private val showReplyComposer = BehaviorSubject.create<Boolean>()
private val scrollToBottom = BehaviorSubject.create<Void>()
private val insertNewReplyToList = PublishSubject.create<Pair<String, DateTime>>()

private val onCommentReplies = BehaviorSubject.create<List<CommentCardData>>()

Expand Down Expand Up @@ -85,6 +94,37 @@ interface ThreadViewModel {
this.onCommentReplies.onNext(it.first.comments?.toCommentCardList(it.second))
}

this.insertNewReplyToList
.distinctUntilChanged()
.withLatestFrom(this.currentUser.loggedInUser()) {
comment, user ->
Pair(comment, user)
}
.withLatestFrom(commentData) {
reply, parent ->
Pair(reply, parent)
}
.map {
Pair(it.second, buildReplyBody(Pair(Pair(it.second, it.first.second), it.first.first)))
}
.map {
CommentCardData.builder()
.comment(it.second)
.project(it.first.project)
.commentableId(it.first.commentableId)
.commentCardState(CommentCardStatus.TRYING_TO_POST.commentCardStatus)
.build()
}
.withLatestFrom(this.onCommentReplies) { reply, list ->
list.toMutableList().apply {
add(reply)
}.toList()
}.compose(bindToLifecycle())
.subscribe {
onCommentReplies.onNext(it)
scrollToBottom.onNext(null)
}

commentData
.compose(bindToLifecycle())
.subscribe {
Expand Down Expand Up @@ -117,6 +157,20 @@ interface ThreadViewModel {
}
}

private fun buildReplyBody(it: Pair<Pair<CommentCardData, User>, Pair<String, DateTime>>): Comment {
return Comment.builder()
.body(it.second.first)
.parentId(it.first.first.comment?.id() ?: -1)
.authorBadges(listOf())
.createdAt(it.second.second)
.cursor("")
.deleted(false)
.id(-1)
.repliesCount(0)
.author(it.first.second)
.build()
}

private fun getCommentComposerStatus(projectAndUser: Pair<Project, User?>) =
when {
projectAndUser.second == null -> CommentComposerStatus.GONE
Expand All @@ -132,8 +186,13 @@ interface ThreadViewModel {
override fun onCommentReplies(): Observable<List<CommentCardData>> = this.onCommentReplies

override fun shouldFocusOnCompose(): Observable<Boolean> = this.focusOnCompose
override fun scrollToBottom(): Observable<Void> = this.scrollToBottom

override fun currentUserAvatar(): Observable<String?> = currentUserAvatar
override fun replyComposerStatus(): Observable<CommentComposerStatus> = replyComposerStatus
override fun showReplyComposer(): Observable<Boolean> = showReplyComposer
override fun insertNewReplyToList(comment: String, createdAt: DateTime) = this.insertNewReplyToList.onNext(
Pair(comment, createdAt)
)
}
}
1 change: 1 addition & 0 deletions app/src/main/res/layout/activity_thread_layout.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
android:layout_height="match_parent">

<androidx.core.widget.NestedScrollView
android:id="@+id/parent_scroll_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:fillViewport="true"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -508,4 +508,37 @@ class CommentsViewHolderViewModelTest : KSRobolectricTestCase() {

this.isCommentReply.assertValue(null)
}

@Test
fun testPostReply_Successful() {
val reply = CommentFactory.reply(createdAt = createdAt)
val currentUser = UserFactory.user().toBuilder().id(1).build()

val env = environment().toBuilder()
.apolloClient(object : MockApolloClient() {
override fun createComment(comment: PostCommentData): Observable<Comment> {
return Observable.just(reply)
}
})
.currentUser(MockCurrentUser(currentUser))
.build()
setUpEnvironment(env)

val comment = CommentFactory.commentToPostWithUser(currentUser)
val commentCardData = CommentCardData.builder()
.comment(comment)
.project(ProjectFactory.initialProject())
.commentableId(ProjectFactory.initialProject().id().toString())
.commentCardState(CommentCardStatus.TRYING_TO_POST.commentCardStatus)
.build()

this.vm.inputs.configureWith(commentCardData)

this.commentCardStatus.assertValues(
CommentCardStatus.TRYING_TO_POST,
CommentCardStatus.COMMENT_FOR_LOGIN_BACKED_USERS
)

this.commentSuccessfullyPosted.assertValue(reply)
}
}

0 comments on commit f30330b

Please sign in to comment.